Conjunto de datos de diágnostico de cáncer de mama en Wisconsin.

1. Análisis Inicial y Preprocesamiento de los Datos.

1.1 Importación y Carga de Datos.

Para comenzar nuestro análisis es necesario realizar la carga de los datos:


data <- read.csv("data/data.csv")

data
NA

Instalamos los paquetes que serán necesarios durante nuestro proyecto:


#install.packages("tidyverse")
#install.packages("caret")
#install.packages("DataExplorer")
#install.packages("dplyr")
#install.packages("ggplot2")
#install.packages("lattice")
#install.packages("psych")
#install.packages("corrplot")
#install.packages("ggcorrplot")

library(caret)
Warning: package ‘caret’ was built under R version 4.4.2Loading required package: ggplot2
Warning: package ‘ggplot2’ was built under R version 4.4.2Loading required package: lattice
library(DataExplorer)
Warning: package ‘DataExplorer’ was built under R version 4.4.2Registered S3 method overwritten by 'htmlwidgets':
  method           from         
  print.htmlwidget tools:rstudio
library()
library(dplyr)
Warning: package ‘dplyr’ was built under R version 4.4.2
Attaching package: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
library(ggplot2)
library(tidyverse)
── Attaching core tidyverse packages ─────────────────────────────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ lubridate 1.9.3     ✔ tibble    3.2.1
✔ purrr     1.0.2     ✔ tidyr     1.3.1
✔ readr     2.1.5     ── Conflicts ───────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
✖ purrr::lift()   masks caret::lift()
ℹ Use the ]8;;http://conflicted.r-lib.org/conflicted package]8;; to force all conflicts to become errors
library(lattice)
library(psych)
Warning: package ‘psych’ was built under R version 4.4.2
Attaching package: ‘psych’

The following objects are masked from ‘package:ggplot2’:

    %+%, alpha
library(corrplot)
Warning: package ‘corrplot’ was built under R version 4.4.2corrplot 0.95 loaded
library(ggcorrplot)
Warning: package ‘ggcorrplot’ was built under R version 4.4.2

Hagamos una vista inicial de los datos:

#primeras filas de nuestro dataset:

head(data)
# Dimensión de nuestro dataset:

dim(data)
[1] 569  33

Nuestro dataset cuenta con 33 columnas y 569 filas.

# Con str mostramos la estructura de nuestro dataframe, incluyendo los tipos de nuestras variables:

str(data)
'data.frame':   569 obs. of  33 variables:
 $ id                     : int  842302 842517 84300903 84348301 84358402 843786 844359 84458202 844981 84501001 ...
 $ diagnosis              : chr  "M" "M" "M" "M" ...
 $ radius_mean            : num  18 20.6 19.7 11.4 20.3 ...
 $ texture_mean           : num  10.4 17.8 21.2 20.4 14.3 ...
 $ perimeter_mean         : num  122.8 132.9 130 77.6 135.1 ...
 $ area_mean              : num  1001 1326 1203 386 1297 ...
 $ smoothness_mean        : num  0.1184 0.0847 0.1096 0.1425 0.1003 ...
 $ compactness_mean       : num  0.2776 0.0786 0.1599 0.2839 0.1328 ...
 $ concavity_mean         : num  0.3001 0.0869 0.1974 0.2414 0.198 ...
 $ concave.points_mean    : num  0.1471 0.0702 0.1279 0.1052 0.1043 ...
 $ symmetry_mean          : num  0.242 0.181 0.207 0.26 0.181 ...
 $ fractal_dimension_mean : num  0.0787 0.0567 0.06 0.0974 0.0588 ...
 $ radius_se              : num  1.095 0.543 0.746 0.496 0.757 ...
 $ texture_se             : num  0.905 0.734 0.787 1.156 0.781 ...
 $ perimeter_se           : num  8.59 3.4 4.58 3.44 5.44 ...
 $ area_se                : num  153.4 74.1 94 27.2 94.4 ...
 $ smoothness_se          : num  0.0064 0.00522 0.00615 0.00911 0.01149 ...
 $ compactness_se         : num  0.049 0.0131 0.0401 0.0746 0.0246 ...
 $ concavity_se           : num  0.0537 0.0186 0.0383 0.0566 0.0569 ...
 $ concave.points_se      : num  0.0159 0.0134 0.0206 0.0187 0.0188 ...
 $ symmetry_se            : num  0.03 0.0139 0.0225 0.0596 0.0176 ...
 $ fractal_dimension_se   : num  0.00619 0.00353 0.00457 0.00921 0.00511 ...
 $ radius_worst           : num  25.4 25 23.6 14.9 22.5 ...
 $ texture_worst          : num  17.3 23.4 25.5 26.5 16.7 ...
 $ perimeter_worst        : num  184.6 158.8 152.5 98.9 152.2 ...
 $ area_worst             : num  2019 1956 1709 568 1575 ...
 $ smoothness_worst       : num  0.162 0.124 0.144 0.21 0.137 ...
 $ compactness_worst      : num  0.666 0.187 0.424 0.866 0.205 ...
 $ concavity_worst        : num  0.712 0.242 0.45 0.687 0.4 ...
 $ concave.points_worst   : num  0.265 0.186 0.243 0.258 0.163 ...
 $ symmetry_worst         : num  0.46 0.275 0.361 0.664 0.236 ...
 $ fractal_dimension_worst: num  0.1189 0.089 0.0876 0.173 0.0768 ...
 $ X                      : logi  NA NA NA NA NA NA ...

# Con Describe podemos ver un primer resumen estadístico básico:

describe(data)
Warning: no non-missing arguments to min; returning InfWarning: no non-missing arguments to max; returning -Inf
# Tipos de datos en nuestras variables:

sapply(data, class)
                     id               diagnosis             radius_mean            texture_mean 
              "integer"             "character"               "numeric"               "numeric" 
         perimeter_mean               area_mean         smoothness_mean        compactness_mean 
              "numeric"               "numeric"               "numeric"               "numeric" 
         concavity_mean     concave.points_mean           symmetry_mean  fractal_dimension_mean 
              "numeric"               "numeric"               "numeric"               "numeric" 
              radius_se              texture_se            perimeter_se                 area_se 
              "numeric"               "numeric"               "numeric"               "numeric" 
          smoothness_se          compactness_se            concavity_se       concave.points_se 
              "numeric"               "numeric"               "numeric"               "numeric" 
            symmetry_se    fractal_dimension_se            radius_worst           texture_worst 
              "numeric"               "numeric"               "numeric"               "numeric" 
        perimeter_worst              area_worst        smoothness_worst       compactness_worst 
              "numeric"               "numeric"               "numeric"               "numeric" 
        concavity_worst    concave.points_worst          symmetry_worst fractal_dimension_worst 
              "numeric"               "numeric"               "numeric"               "numeric" 
                      X 
              "logical" 
# Veamos si existen valores faltantes en nuestros datos:

anyNA(data)
[1] TRUE
#Contamos el numero de valores faltantes por columna:

colSums(is.na(data))
                     id               diagnosis             radius_mean            texture_mean 
                      0                       0                       0                       0 
         perimeter_mean               area_mean         smoothness_mean        compactness_mean 
                      0                       0                       0                       0 
         concavity_mean     concave.points_mean           symmetry_mean  fractal_dimension_mean 
                      0                       0                       0                       0 
              radius_se              texture_se            perimeter_se                 area_se 
                      0                       0                       0                       0 
          smoothness_se          compactness_se            concavity_se       concave.points_se 
                      0                       0                       0                       0 
            symmetry_se    fractal_dimension_se            radius_worst           texture_worst 
                      0                       0                       0                       0 
        perimeter_worst              area_worst        smoothness_worst       compactness_worst 
                      0                       0                       0                       0 
        concavity_worst    concave.points_worst          symmetry_worst fractal_dimension_worst 
                      0                       0                       0                       0 
                      X 
                    569 
plot_missing(data)

Como puede observarse, contamos con 569 valores faltantes en la última columna “X”. Más adelante veremos como tratarlo.

1.2 Análisis exploratorio de los datos

En este paso nuestro objetivo será entender la distribución y relaciones de variables.

Visualización de distribuciones y correlaciones:

#Veamos un resumen gráfico general:

plot_intro(data)

#Variables Categóricas:

plot_bar(data)

#Variables Numéricas
plot_histogram(data)

1.3 Preprocesamiento de los datos.

Eliminación de valores faltantes:

Para comenzar, ya sabemos que existe una columna cuyos valores son todos NA, es decir, faltantes. El primero paso en nuestro preprocesamiento será eliminar esta columna “x”:

data <- read.csv("data/data.csv")
data
head(data)
data <- data %>% select(-X)

Veamos si se ha borrado correctamente la columna X y si ahora existe algún otro valor faltante:

colnames(data)
 [1] "id"                      "diagnosis"               "radius_mean"            
 [4] "texture_mean"            "perimeter_mean"          "area_mean"              
 [7] "smoothness_mean"         "compactness_mean"        "concavity_mean"         
[10] "concave.points_mean"     "symmetry_mean"           "fractal_dimension_mean" 
[13] "radius_se"               "texture_se"              "perimeter_se"           
[16] "area_se"                 "smoothness_se"           "compactness_se"         
[19] "concavity_se"            "concave.points_se"       "symmetry_se"            
[22] "fractal_dimension_se"    "radius_worst"            "texture_worst"          
[25] "perimeter_worst"         "area_worst"              "smoothness_worst"       
[28] "compactness_worst"       "concavity_worst"         "concave.points_worst"   
[31] "symmetry_worst"          "fractal_dimension_worst"
# Veamos si existen valores faltantes en nuestros datos:

anyNA(data)
[1] FALSE
#Contamos el numero de valores faltantes por columna:

colSums(is.na(data))
                     id               diagnosis             radius_mean            texture_mean 
                      0                       0                       0                       0 
         perimeter_mean               area_mean         smoothness_mean        compactness_mean 
                      0                       0                       0                       0 
         concavity_mean     concave.points_mean           symmetry_mean  fractal_dimension_mean 
                      0                       0                       0                       0 
              radius_se              texture_se            perimeter_se                 area_se 
                      0                       0                       0                       0 
          smoothness_se          compactness_se            concavity_se       concave.points_se 
                      0                       0                       0                       0 
            symmetry_se    fractal_dimension_se            radius_worst           texture_worst 
                      0                       0                       0                       0 
        perimeter_worst              area_worst        smoothness_worst       compactness_worst 
                      0                       0                       0                       0 
        concavity_worst    concave.points_worst          symmetry_worst fractal_dimension_worst 
                      0                       0                       0                       0 
plot_missing(data)

Además de la columna con valores Faltantes, también tenemos una columna “ID” que no nos aporta ninguna información por lo que también procederemos a eliminarla:

data <- data %>% select(-id)
colnames(data)
 [1] "diagnosis"               "radius_mean"             "texture_mean"           
 [4] "perimeter_mean"          "area_mean"               "smoothness_mean"        
 [7] "compactness_mean"        "concavity_mean"          "concave.points_mean"    
[10] "symmetry_mean"           "fractal_dimension_mean"  "radius_se"              
[13] "texture_se"              "perimeter_se"            "area_se"                
[16] "smoothness_se"           "compactness_se"          "concavity_se"           
[19] "concave.points_se"       "symmetry_se"             "fractal_dimension_se"   
[22] "radius_worst"            "texture_worst"           "perimeter_worst"        
[25] "area_worst"              "smoothness_worst"        "compactness_worst"      
[28] "concavity_worst"         "concave.points_worst"    "symmetry_worst"         
[31] "fractal_dimension_worst"
head(data)
NA

Como podemos observar, se ha eliminado la columna “id” y se ha verificado que no existen mas valores faltantes exceptos los ya eliminados en “X”.

Después de ellos contamos con un dataset de:

dim(data)
[1] 569  31

569 filas y 31 columnas.

Codificación de variables Categóricas

Anteriormente vimos que nuestro dataset cuenta con una única columna de valores categóricos. “Diagnosis” cuyos valores tienen el siguiente significado: + M (malignant) + B (benign)

El siguiente paso en el preprocesado de datos será pasar esta columna a numérica. Como solo se presentan dos posibles valores (M y B), se aplicará Codificación Binaria: El valor “M” pasará a ser 1 y valor “B” pasará a ser 0.

Aunque más adelante volveremos a pasarla a categórica, ahora es necesario hacerla binaria para poder estudiar la correlación de las variables.


#M -> 1; B -> 0
data$diagnosis <- ifelse(data$diagnosis == "M", 1, 0)

Vamos a verificar que se ha realizado correctamente la conversión:

table(data$diagnosis)

  0   1 
357 212 
print(data$diagnosis)
  [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 0
 [50] 0 0 0 0 1 1 0 1 1 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 0 1 0 1 1 0 0 0 1 1 0 1 1 1 0 0 0 1 0 0 1 1 0 0
 [99] 0 1 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 1 1 0 0 0 1 1 0 1 0 1 1 0 1 1 0 0 1 0 0 1 0 0 0 0 1
[148] 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 1 1 1 0 1 0 1 0 0 0 1 0 0 1 1 0
[197] 1 1 1 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 1 0 0 1 1 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 1 1 0 1 0 0 0 0 1
[246] 0 0 0 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0
[295] 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 1 1 0 0 0 0 1 0 1 0 1 0 0 0
[344] 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0
[393] 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0
[442] 1 0 0 1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 1
[491] 0 0 1 0 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 1 0 0 1 0 1 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0
[540] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0

Nos cercioramos de que no existe ningún otro valor categórico en el dataset:


categorical_columns <- sapply(data, is.factor) | sapply(data, is.character)
names(data)[categorical_columns]
character(0)

Efectivamente, todas nuestras columnas ahora son numéricas, lo que nos da paso al siguiente punto en nuestro preprocesamiento de datos.

Estudio de correlación.

Al tener nuestro dataset limpio de NA y solo presentes variables numéricas, el siguiente paso será estudiar las posibles correlaciones de nuestro dataset.

Estudiar la correlación de los datos ayuda a identificar patrones y relaciones entre variables, lo que podría conducir a nuevas hipótesis y descubrimientos. Además, un buen estudio de correlaciones podría ser útil para seleccionar variables relevantes y construir modelos en un futuro.

Los valores que obtendremos tendrán la siguiente interpretación:


# Graficar la matriz de correlación con valores en las celdas
ggcorrplot(correlation_matrix, 
           method = "circle",  # Utilizar círculos para representar correlaciones
           type = "lower",     # Mostrar solo la mitad inferior
           lab = TRUE,         # Añadir los valores de correlación
           lab_size = 2,       # Tamaño del texto en las celdas
           colors = c("blue", "white", "red"), # Colores para las correlaciones negativas, neutrales y positivas
           title = "Matriz de Correlación",  # Título del gráfico
           tl.cex = 10)       # Tamaño de las etiquetas

Como puede observarse, al tratarese un conjunto de datos con 31 variables, la matriz de correlación cuesta interpretarla.

Por ello, como “diagnosis” es nuestra variable objetivo, vamos a observar cómo se correlacionan las demás variables con ella:

# Calcular la correlación entre cada variable numérica y 'diagnosis'
cor_with_target <- cor(data, data$diagnosis, use = "complete.obs")

# Crear un data frame para ver las correlaciones junto con los nombres de las variables
correlation_df <- data.frame(Variable = names(data), Correlation = cor_with_target)

# Ordenar el data frame por la columna de Correlation en orden descendente
correlation_df_sorted <- correlation_df[order(-correlation_df$Correlation), ]

# Ver las correlaciones ordenadas junto con los nombres de las variables
print(correlation_df_sorted)
NA
NA

En esta tabla podemos ver la correlación de cada variable con “diagnosis” en orden descendente.

Selección de Atributos:

Una de las formas de hacer una correcta selección de atributos es inspeccionar la anterior tabla de correlación:

Como puede observarse, no existe ninguna variable que supere el 0,8, cuando las variables se correlacionan con la variable clase, en nuestro caso diagnosis, con más de un 0,9, suele ser conveniente eliminarlas para evitar redundancia, lo que no es nuestro caso.

Por otro lado, tampoco tenemos valores demasiado bajos ni negativos, los valores negativos en nuestro caso, podrían ayudar al entrenamiento de nuestro modelo, es por ello, que de momento no se eliminarán variables del dataset.

2. Análisis Supervisado.

El aprendizaje supervisado, también conocido como machine learning supervisado, es una subcategoría del machine learning y la inteligencia artificial. Se define por el uso de conjuntos de datos etiquetados para entrenar algoritmos que clasifican los datos o predicen los resultados con precisión.

A medida que se introducen datos en el modelo, éste ajusta sus ponderaciones hasta que el modelo se ha ajustado adecuadamente, lo que ocurre como parte del proceso de validación cruzada.

En nuestro análisis, la variable a predecir será “diagnosis”, que como vimos anteriormente, es de tipo binario, tomando 1 cuando el tumor es maligno, y 0 cuando el tumor resulta benigno.

En este análisis se han evaluado diferentes modelos de clasificación para un conjunto de datos binario con 569 muestras y 30 predictores, clasificados en dos categorías: ‘Negative’ y ‘Positive’. Los modelos analizados son el Árbol de Decisión (CART), Random Forest (Bosque Aleatorio), Máquinas de Soporte Vectorial con núcleo Radial (SVM), y el Modelo Lineal Generalizado (GLM).

El primer paso para el análisis supervisado es realizar la divión del dataset.

2.1 División del Dataset:

Para este paso, usaremos la validación cruzada k-fold para dividir el dataset y evaluar el modelo. Aunque podríamos dividir el conjunto en un 80% train y un 20% train, valores típicos, se ha optado por utilizar K-fold ya que asegura que cada subconjunto o fold del dataset es utilizado tanto para el entrenamiento como para prueba en distintas iteraciones, lo que nos proporcionará una evaluación más robusta.

#Cargamos la librería caret
library(caret)

#Configuramos la validación cruzada K-fold
set.seed(123)

k <- 5 

# Configurar K-fold Cross-Validation con probabilidades de clase
train_control <- trainControl(
  method = "cv",           # Cross-validation
  number = k,              # Número de pliegues (folds)
  classProbs = TRUE,       # Habilitar probabilidades de clase
  summaryFunction = twoClassSummary, # Para métricas de clasificación binaria
  savePredictions = "final" # Guardar las predicciones finales
)

2.2 Entrenamiento del modelo:

Cambiamos los niveles del factor “diagnosis” a nombres válidos, en este caso “M” para los tumores malignos, “B” para aquellos benignos.

# Entrenar el modelo de Random Forest utilizando K-fold cross-validation
set.seed(123)

#Vamos a convertir
data$diagnosis <- factor(data$diagnosis, levels = c(0, 1), labels = c("B", "M"))

2.2.1 Árbol de Decisión (CART):

El árbol de decisión es un modelo simple que divide los datos en segmentos basados en reglas de decisión. Es útil para clasificaciones donde las decisiones son lógicas y fáciles de entender


# Árbol de Decisión utilizando K-fold cross-validation
model_cart <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data,
  method = "rpart",     # Árbol de decisión (CART)
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_cart)
CART 

569 samples
 30 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 456, 455, 454, 456 
Resampling results across tuning parameters:

  cp           ROC        Sens       Spec     
  0.004716981  0.9430688  0.9410798  0.8911406
  0.049528302  0.9119667  0.8990219  0.9105205
  0.792452830  0.7258972  0.9464789  0.5053156

ROC was used to select the optimal model using the largest value.
The final value used for the model was cp = 0.004716981.
# Extraer la importancia de las variables
importance_cart <- varImp(model_cart, scale = FALSE)
importance_cart_df <- importance_cart$importance
importance_cart_df$variable <- rownames(importance_cart_df)

El primer modelo analizado es el Árbol de Decisión (CART). Este modelo utiliza un valor de complejidad de poda (cp) de 0.0047, que fue seleccionado como el mejor parámetro mediante validación cruzada.

La curva ROC del modelo es de 0.9362, lo que indica una alta capacidad para discriminar entre las dos clases.

La sensibilidad (capacidad del modelo para identificar correctamente las observaciones positivas) es de 0.9383, lo que sugiere que el modelo es muy eficiente para detectar las observaciones positivas, aunque algo menos efectivo que otros modelos en cuanto a la especificidad. De hecho, la especificidad (capacidad para identificar correctamente las observaciones negativas) es de 0.8960, lo que representa una leve caída respecto a la sensibilidad.

Este desempeño es sólido y equilibrado, pero no es el más alto entre los modelos evaluados.

<font size="2">2.2.2 Random Forest </font> 

El Random Forest es un algoritmo que construye múltiples árboles de decisión y realiza una predicción agregando las predicciones de todos los árboles individuales. Es robusto ante el sobreajuste.

model_rf <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data,
  method = "rf",        # Random Forest
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_rf)
Random Forest 

569 samples
 30 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 454, 454, 456, 456, 456 
Resampling results across tuning parameters:

  mtry  ROC        Sens       Spec     
   2    0.9926860  0.9803991  0.9198228
  16    0.9910848  0.9831768  0.9341085
  30    0.9900850  0.9719484  0.9341085

ROC was used to select the optimal model using the largest value.
The final value used for the model was mtry = 2.
# Extraer la importancia de las variables
importance_rf <- varImp(model_rf, scale = FALSE)
importance_rf_df <- importance_rf$importance
importance_rf_df$variable <- rownames(importance_rf_df)

Para entender mejor los resultados del modelo, aclarar que el parámetro mtry en el contexto de Random Forest es uno de los hiperparámetros clave que se utiliza para controlar el número de variables (características) que el modelo considera para dividir cada nodo en cada árbol del bosque. Específicamente, mtry define cuántas características serán elegidas aleatoriamente para cada nodo cuando se construye un árbol en el Random Forest.

Random Forest, muestra un desempeño destacable. Este modelo seleccionó el valor de mtry (número de variables aleatorias para cada división del árbol) igual a 2, lo que optimiza la capacidad de discriminación. Su curva ROC alcanza un valor impresionante de 0.9908, lo que es un indicador claro de su capacidad para separar las dos clases con gran precisión. Además, la sensibilidad de 0.9804 muestra que Random Forest tiene una excelente capacidad para detectar correctamente las observaciones positivas, y la especificidad de 0.9241 indica que también es eficaz en identificar las observaciones negativas. Este modelo sobresale por su alta precisión en ambos aspectos, lo que lo convierte en uno de los modelos más robustos y confiables para este conjunto de datos.

2.2.3 Support Vector Machine (SVM)

El Support Vector Machine (SVM) es un algoritmo que intenta encontrar un hiperplano que mejor separe las diferentes clases de datos.

#install.packages("kernlab")
library(kernlab)

model_svm <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data,
  method = "svmRadial",  # Support Vector Machine con kernel radial
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_svm)
Support Vector Machines with Radial Basis Function Kernel 

569 samples
 30 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 454, 456, 456, 455 
Resampling results across tuning parameters:

  C     ROC        Sens       Spec     
  0.25  0.9921417  0.9692488  0.9338870
  0.50  0.9937241  0.9692879  0.9529347
  1.00  0.9953659  0.9748826  0.9624585

Tuning parameter 'sigma' was held constant at a value of 0.04441149
ROC was used to select the optimal model using the largest value.
The final values used for the model were sigma = 0.04441149 and C = 1.
# Extraer la importancia de las variables
importance_svm <- varImp(model_svm, scale = FALSE)
importance_svm_df <- importance_svm$importance
importance_svm_df$variable <- rownames(importance_svm_df)

El tercer modelo evaluado es el de Máquinas de Soporte Vectorial con núcleo Radial (SVM). Este modelo, con un parámetro de regularización 𝐶=1 y un valor de sigma de 0.0475, mostró un desempeño excelente en términos de la curva ROC, alcanzando un valor de 0.9948, el más alto entre todos los modelos. Esta métrica refleja una capacidad de discriminación superior, lo que implica que el modelo tiene una alta habilidad para separar correctamente las clases ‘Negative’ y ‘Positive’. La sensibilidad de 0.9748 y la especificidad de 0.9670 también son notablemente altas, lo que sugiere que el modelo tiene un buen rendimiento tanto en la detección de las observaciones positivas como en la correcta identificación de las negativas. Sin embargo, es importante destacar que el modelo SVM presentó varias advertencias durante el proceso de optimización (warnings), relacionadas con problemas de convergencia y probabilidades extremas de 0 o 1. Esto podría indicar que el modelo podría estar sobreajustando o enfrentando dificultades para encontrar un equilibrio estable, lo que debe tenerse en cuenta al evaluar su estabilidad y generalización.

2.2.4 Regresión Logística

Este algoritmo es un modelo de clasificación que predice la probabilidad de que una observación pertenzca a una clase o no.



model_logit <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data,
  method = "glm",       # Regresión Logística
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)
Warning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurred
# Ver el resumen del modelo entrenado
print(model_logit)
Generalized Linear Model 

569 samples
 30 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 455, 456, 455, 455 
Resampling results:

  ROC        Sens      Spec     
  0.9527876  0.946831  0.9434109
# Extraer la importancia de las variables
importance_logit <- varImp(model_logit, scale = FALSE)
importance_logit_df <- importance_logit$importance
importance_logit_df$variable <- rownames(importance_logit_df)

El último modelo considerado es el Modelo Lineal Generalizado (GLM). Este modelo, a pesar de ser sencillo en su estructura, mostró un rendimiento respetable. Su curva ROC alcanzó un valor de 0.9552, lo cual es inferior a los de Random Forest y SVM, pero sigue siendo adecuado para tareas de clasificación. La sensibilidad de 0.9438 indica que el modelo tiene una buena capacidad para identificar las observaciones positivas, mientras que la especificidad de 0.9484 es ligeramente mejor que la sensibilidad, lo que sugiere que el modelo tiene un desempeño ligeramente mejor para detectar las observaciones negativas en comparación con las positivas. Aunque el modelo GLM es funcional, su rendimiento en términos de la curva ROC es algo inferior en comparación con los modelos más complejos como SVM y Random Forest.

2.3 Comparación de Modelos

# Comparar el rendimiento de los modelos
resamples <- resamples(list(cart = model_cart, rf = model_rf, svm = model_svm, logit = model_logit))

# Ver el resumen de la comparación entre los modelos
summary(resamples)

Call:
summary.resamples(object = resamples)

Models: cart, rf, svm, logit 
Number of resamples: 5 

ROC 
           Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
cart  0.9299050 0.9330986 0.9357814 0.9430688 0.9467593 0.9697997    0
rf    0.9820736 0.9902750 0.9953052 0.9926860 0.9964470 0.9993293    0
svm   0.9874031 0.9947593 0.9976526 0.9953659 0.9976852 0.9993293    0
logit 0.9365079 0.9434982 0.9495701 0.9527876 0.9610219 0.9733400    0

Sens 
           Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
cart  0.9014085 0.9295775 0.9577465 0.9410798 0.9583333 0.9583333    0
rf    0.9583333 0.9718310 0.9718310 0.9803991 1.0000000 1.0000000    0
svm   0.9305556 0.9718310 0.9859155 0.9748826 0.9861111 1.0000000    0
logit 0.9027778 0.9295775 0.9577465 0.9468310 0.9718310 0.9722222    0

Spec 
           Min.   1st Qu.    Median      Mean   3rd Qu.      Max. NA's
cart  0.8571429 0.8571429 0.8809524 0.8911406 0.9069767 0.9534884    0
rf    0.8604651 0.8809524 0.9285714 0.9198228 0.9523810 0.9767442    0
svm   0.9302326 0.9534884 0.9761905 0.9624585 0.9761905 0.9761905    0
logit 0.9047619 0.9302326 0.9523810 0.9434109 0.9534884 0.9761905    0
# Visualización de la comparación
bwplot(resamples)

2.4 Estudio de la Importancia de las Variables

El estudio de la importancia de variables nos permite identificar cuáles son las características que más influyen en las predicciones realizadas. Esto es especialmente útil cuando trabjamos con datos de ámbito sanitario, ya que nos permite priorizar las variables más relevantes para realizar diagnósticos o tomar decisiones informadas.

El objetivo del estudio de importancia de variables es eliminar aquellas que consistentemente tienen baja importancia en todos los modelos:

1- Obtener la importancia de variables de cada modelo.

2.4.1 Estudio de la Importancia de las Variables con Árbol de Decisión (CART)

En los árboles de decisión, la importancia de vairables se calcula en función de las ganancias de reducción de impureza, por ejemplo, la reducción de la entropía o del índice Gini en los nodos donde la variables es utilizada para dividr los datos.

plot(importance_cart, main = "Importancia de Variables - CART")

2.4.2 Estudio de la Importancia de las Variables con Random Forest

En Random Forest la importancia se calcula mediante dos enfoques comunes:

plot(importance_rf, main = "Importancia de Variables - Random Forest")

2.4.3 Estudio de la Importancia de las Variables con Support Vector Machine (SVM)

El calculo de la importancia en SVM no es tan directo, ya que este modelo no se basa en una estructura jerárquica o en una gregación de árboles. Podemos estimar la importancia de las variables mediante análisis post-hoc, como la evaluación de los coeficientes en el espacio formado por el núcleo radial.

plot(importance_svm, main = "Importancia de Variables- SVM")

2.4.4 Estudio de la Importancia de las Variables con Regresión Logística

En regresión logística, la importancia de variables se puede analizar mediante los coeficientes estimados del modelo. Estos coeficientes indican la magnitud y la dirección del efecto de cada variable en la probabilidad de que un tumor sea maligno.

plot(importance_logit, main = "Importancia de Variables - GLM")

2.4.4 Comparación General del estudio de la Importancia de las Variables

2- Normalizar la importancia para compararla entre modelos. 3- Calcular una métrica consolidada de importancia promedio. 4- Identificar las variables con menor impacto en todos los modelos.

# Consolidar importancia de variables
importance_combined <- merge(
  merge(importance_cart_df, importance_rf_df, by = "variable", suffixes = c("_cart", "_rf")),
  merge(importance_svm_df, importance_logit_df, by = "variable", suffixes = c("_svm", "_logit")),
  by = "variable"
)

# Promedio de importancia
importance_combined$mean_importance <- rowMeans(importance_combined[, -1], na.rm = TRUE)

# Seleccionar las menos importantes (por debajo de un umbral, por ejemplo, el percentil 20)
threshold <- quantile(importance_combined$mean_importance, 0.2)
least_important_vars <- importance_combined$variable[importance_combined$mean_importance <= threshold]

# Eliminar estas variables del dataset original
data_reduced <- data[, !(names(data) %in% least_important_vars)]

# Guardar el dataset reducido
write.csv(data_reduced, "data_reduced.csv", row.names = FALSE)
data_reduced
dim(data_reduced)
[1] 569  25
colnames(data_reduced)
 [1] "diagnosis"            "texture_mean"         "perimeter_mean"      
 [4] "area_mean"            "concavity_mean"       "concave.points_mean" 
 [7] "symmetry_mean"        "radius_se"            "texture_se"          
[10] "perimeter_se"         "area_se"              "smoothness_se"       
[13] "compactness_se"       "concavity_se"         "concave.points_se"   
[16] "symmetry_se"          "fractal_dimension_se" "radius_worst"        
[19] "texture_worst"        "perimeter_worst"      "area_worst"          
[22] "compactness_worst"    "concavity_worst"      "concave.points_worst"
[25] "symmetry_worst"      
colnames(data)
 [1] "diagnosis"               "radius_mean"            
 [3] "texture_mean"            "perimeter_mean"         
 [5] "area_mean"               "smoothness_mean"        
 [7] "compactness_mean"        "concavity_mean"         
 [9] "concave.points_mean"     "symmetry_mean"          
[11] "fractal_dimension_mean"  "radius_se"              
[13] "texture_se"              "perimeter_se"           
[15] "area_se"                 "smoothness_se"          
[17] "compactness_se"          "concavity_se"           
[19] "concave.points_se"       "symmetry_se"            
[21] "fractal_dimension_se"    "radius_worst"           
[23] "texture_worst"           "perimeter_worst"        
[25] "area_worst"              "smoothness_worst"       
[27] "compactness_worst"       "concavity_worst"        
[29] "concave.points_worst"    "symmetry_worst"         
[31] "fractal_dimension_worst"

El siguiente paso será repetir nuestro análisis supervisado esta vez usando el dataset reducido en el que no aparecen las columnas “menos importantes”. De esta forma podremos analizar si los resultados mejorar, empeoran o no afectan con el estudio de importancia de varianles:

3. Análisis Supervisado con dataset reducido. 3.1 Árbol de Decisión (CART)

# Árbol de Decisión utilizando K-fold cross-validation
model_cart <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data_reduced,
  method = "rpart",     # Árbol de decisión (CART)
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_cart)
CART 

569 samples
 24 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 456, 456, 454, 455 
Resampling results across tuning parameters:

  cp           ROC        Sens       Spec     
  0.004716981  0.9450967  0.9523474  0.8914729
  0.049528302  0.8894831  0.9298513  0.8535991
  0.792452830  0.7976289  0.9467527  0.6485050

ROC was used to select the optimal model using the largest value.
The final value used for the model was cp = 0.004716981.

3.2 Random Forest

model_rf <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data_reduced,
  method = "rf",        # Random Forest
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_rf)
Random Forest 

569 samples
 24 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 454, 455, 456, 456 
Resampling results across tuning parameters:

  mtry  ROC        Sens       Spec     
   2    0.9908968  0.9690923  0.9295681
  13    0.9876974  0.9634194  0.9387597
  24    0.9864531  0.9577856  0.9293466

ROC was used to select the optimal model using the largest value.
The final value used for the model was mtry = 2.

3.3 Support Vector Machine (SVM)

#install.packages("kernlab")
library(kernlab)

model_svm <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data_reduced,
  method = "svmRadial",  # Support Vector Machine con kernel radial
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)

# Ver el resumen del modelo entrenado
print(model_svm)
Support Vector Machines with Radial Basis Function Kernel 

569 samples
 24 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 454, 456, 455, 456, 455 
Resampling results across tuning parameters:

  C     ROC        Sens       Spec     
  0.25  0.9913841  0.9663928  0.9478405
  0.50  0.9938446  0.9692097  0.9621262
  1.00  0.9946373  0.9692097  0.9621262

Tuning parameter 'sigma' was held constant at a value of 0.05850564
ROC was used to select the optimal model using the largest value.
The final values used for the model were sigma = 0.05850564 and C = 1.

3.4 Regresión Logística

model_logit <- train(
  diagnosis ~ .,        # Usamos todas las variables predictoras
  data = data_reduced,
  method = "glm",       # Regresión Logística
  trControl = train_control,  # Control de validación cruzada
  metric = "ROC"        # Evaluar utilizando AUC (Área bajo la curva ROC)
)
Warning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: algorithm did not convergeWarning: glm.fit: fitted probabilities numerically 0 or 1 occurredWarning: glm.fit: fitted probabilities numerically 0 or 1 occurred
# Ver el resumen del modelo entrenado
print(model_logit)
Generalized Linear Model 

569 samples
 24 predictor
  2 classes: 'B', 'M' 

No pre-processing
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 455, 454, 455, 456, 456 
Resampling results:

  ROC       Sens       Spec     
  0.975885  0.9662754  0.9579181

3.5 Comparación de resultados análisis supervisado dataset incial vs dataset reducido:



# Crear los datos para cada modelo en formato de data frame
# Dataset inicial
inicial <- data.frame(
  Modelo = c("CART", "Random Forest", "SVM", "GLM"),
  ROC = c(0.9431, 0.9927, 0.9954, 0.9528),
  Sens = c(0.9411, 0.9804, 0.9749, 0.9468),
  Spec = c(0.8911, 0.9198, 0.9625, 0.9434),
  Dataset = "Inicial"
)

# Dataset reducido
reducido <- data.frame(
  Modelo = c("CART", "Random Forest", "SVM", "GLM"),
  ROC = c(0.9451, 0.9909, 0.9946, 0.9759),
  Sens = c(0.9523, 0.9691, 0.9692, 0.9663),
  Spec = c(0.8915, 0.9296, 0.9621, 0.9579),
  Dataset = "Reducido"
)

# Unir ambos datasets en una sola tabla por el modelo
comparativa <- merge(inicial, reducido, by = "Modelo")

# Imprimir la tabla en consola
print(comparativa)
NA
# Instalar flextable si no está instalado
install.packages("flextable")
WARNING: Rtools is required to build R packages but is not currently installed. Please download and install the appropriate version of Rtools before proceeding:

https://cran.rstudio.com/bin/windows/Rtools/
Installing package into ‘C:/Users/hered/AppData/Local/R/win-library/4.4’
(as ‘lib’ is unspecified)
also installing the dependencies ‘fontBitstreamVera’, ‘fontLiberation’, ‘fontquiver’, ‘zip’, ‘gdtools’, ‘officer’

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/fontBitstreamVera_0.1.1.zip'
Content type 'application/zip' length 697821 bytes (681 KB)
downloaded 681 KB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/fontLiberation_0.1.0.zip'
Content type 'application/zip' length 4530297 bytes (4.3 MB)
downloaded 4.3 MB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/fontquiver_0.2.1.zip'
Content type 'application/zip' length 2279308 bytes (2.2 MB)
downloaded 2.2 MB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/zip_2.3.1.zip'
Content type 'application/zip' length 434674 bytes (424 KB)
downloaded 424 KB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/gdtools_0.4.1.zip'
Content type 'application/zip' length 2207516 bytes (2.1 MB)
downloaded 2.1 MB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/officer_0.6.7.zip'
Content type 'application/zip' length 1839413 bytes (1.8 MB)
downloaded 1.8 MB

trying URL 'https://cran.rstudio.com/bin/windows/contrib/4.4/flextable_0.9.7.zip'
Content type 'application/zip' length 2357184 bytes (2.2 MB)
downloaded 2.2 MB
package ‘fontBitstreamVera’ successfully unpacked and MD5 sums checked
package ‘fontLiberation’ successfully unpacked and MD5 sums checked
package ‘fontquiver’ successfully unpacked and MD5 sums checked
package ‘zip’ successfully unpacked and MD5 sums checked
package ‘gdtools’ successfully unpacked and MD5 sums checked
package ‘officer’ successfully unpacked and MD5 sums checked
package ‘flextable’ successfully unpacked and MD5 sums checked

The downloaded binary packages are in
    C:\Users\hered\AppData\Local\Temp\RtmpWuj0fH\downloaded_packages
# Cargar la librería
library(flextable)
Warning: package ‘flextable’ was built under R version 4.4.2
Attaching package: ‘flextable’

The following object is masked from ‘package:purrr’:

    compose
# Crear la tabla con flextable
tabla_flex <- flextable(comparativa)

# Imprimir la tabla en un entorno que soporte flextable (por ejemplo, RStudio)
tabla_flex

Modelo

ROC.x

Sens.x

Spec.x

Dataset.x

ROC.y

Sens.y

Spec.y

Dataset.y

CART

0.9431

0.9411

0.8911

Inicial

0.9451

0.9523

0.8915

Reducido

GLM

0.9528

0.9468

0.9434

Inicial

0.9759

0.9663

0.9579

Reducido

Random Forest

0.9927

0.9804

0.9198

Inicial

0.9909

0.9691

0.9296

Reducido

SVM

0.9954

0.9749

0.9625

Inicial

0.9946

0.9692

0.9621

Reducido

NA

La sensibilidad mejora con el dataset reducido, lo que indica una mejor capacidad para detectar casos positivos. Esto puede deberse a que las variables menos relevantes generaban divisiones innecesarias en el árbol.

Aunque hay una leve disminución en ROC y sensibilidad, la especificidad mejora. Esto implica que el modelo con el dataset reducido identifica mejor los casos negativos.

El impacto del dataset reducido es mínimo, pero muestra una ligera disminución en todas las métricas. Esto sugiere que las variables eliminadas tenían una influencia menor, pero no eran completamente irrelevantes.

El modelo con dataset reducido muestra mejoras claras en todas las métricas, lo que refuerza la idea de que las variables eliminadas no eran útiles para este enfoque lineal.

En resumen: La eliminación de variables menos importantes mantiene un rendimiento comparable, e incluso mejora algunas métricas específicas en ciertos modelos.

Esto sugiere que las variables eliminadas no aportaban información relevante o incluso podían introducir ruido.

Reducir el número de variables tiene ventajas prácticas, como menores requerimientos computacionales y mayor interpretabilidad, sin comprometer el rendimiento.

4. Análisis No Supervisado.

El análisis no supervisado se centra en explorar patrones o estructuras en los datos sin utlizar etiquetas o clases. Para éste análisis, se utulizarán técnicas como clustering o reducción de dimensionalidad.

4.1 Preparación del Dataset

El primer paso será eliminar la columna “diagnosis” y normalizar el dataset, ya que muchas técnicas de clustering y reducción de dimensionalidad son sensibles a las escalas de las variables.

data_UnSupervised <- data %>% select(-diagnosis)
head(data_UnSupervised)
data_UnSupervised$diagnosis
NULL
# Normalizar los datos (sin la variable de respuesta si existe)
preprocess_params <- preProcess(data_UnSupervised[, -ncol(data_UnSupervised)], method = c("center", "scale"))
data_normalized <- predict(preprocess_params, data_UnSupervised[, -ncol(data_UnSupervised)])

4.2 Clustering: Determinación del Número Óptimo de Clusters

Para ello utilizaremos el método del codo y el índice silhouette:

# Método del codo
wss <- (nrow(data_normalized) - 1) * sum(apply(data_normalized, 2, var))
for (i in 2:15) wss[i] <- sum(kmeans(data_normalized, centers = i)$withinss)

plot(1:15, wss, type = "b", xlab = "Número de Clusters", ylab = "Suma de Cuadrados Internos")


# Índice silhouette
library(cluster)
silhouette_scores <- numeric()
for (i in 2:15) {
  km <- kmeans(data_normalized, centers = i)
  silhouette_scores[i] <- mean(silhouette(km$cluster, dist(data_normalized))[, 3])
}
plot(2:15, silhouette_scores[-1], type = "b", xlab = "Número de Clusters", ylab = "Puntaje de Silhouette")

4.3 K-Means

Una vez determinado el número óptimo de clusters, se ejecuta K-means:

set.seed(123)

optimal_clusters = 4

kmeans_model <- kmeans(data_normalized, centers = optimal_clusters, nstart = 25)

# Agregar etiquetas de cluster al dataset original
data$cluster <- kmeans_model$cluster

4.4 PCA

El próximo paso de nuestro análisis no supervisado será utilizar el Análisis de Componentes Principales para visualizar la estructura de los datos y validar los resultados del clustering.

# PCA
pca_model <- prcomp(data_normalized, scale = TRUE)

# Visualización de los dos primeros componentes principales
library(ggplot2)
pca_data <- data.frame(pca_model$x[, 1:2], cluster = as.factor(data$cluster))
ggplot(pca_data, aes(x = PC1, y = PC2, color = cluster)) +
  geom_point() +
  theme_minimal() +
  labs(title = "Clustering visualizado en espacio PCA")

Se visualizan claramente cuatro clústeres.

Los clústeres están bien definidos y separados espacialmente, especialmente entre el clúster púrpura y los demás.

Hay un grado menor de solapamiento entre los clústeres, lo que sugiere que las variables originales ofrecen una separación más clara entre las clases en este espacio. Respecto al espacio de Variación, los puntos están distribuidos en torno a coordenadas más centradas (de -15 a 5 en PC1 y -10 a 10 en PC2).

Vamos también a repetir el clustering con el dataset reducido generado durante el estudio de la importancia de las variables:

4.5 Análisis No Supervisado con Dataset Reducido según importancia de variables

data_UnSupervised_reduced <- data_reduced %>% select(-diagnosis)
head(data_UnSupervised_reduced)
data_UnSupervised_reduced$diagnosis
NULL
# Normalizar los datos (sin la variable de respuesta si existe)
preprocess_params <- preProcess(data_UnSupervised_reduced[, -ncol(data_UnSupervised_reduced)], method = c("center", "scale"))
data_normalized_reduced <- predict(preprocess_params, data_UnSupervised_reduced[, -ncol(data_UnSupervised_reduced)])
# Método del codo
wss <- (nrow(data_normalized_reduced) - 1) * sum(apply(data_normalized_reduced, 2, var))
for (i in 2:15) wss[i] <- sum(kmeans(data_normalized_reduced, centers = i)$withinss)

plot(1:15, wss, type = "b", xlab = "Número de Clusters", ylab = "Suma de Cuadrados Internos")


# Índice silhouette
library(cluster)
silhouette_scores <- numeric()
for (i in 2:15) {
  km <- kmeans(data_normalized_reduced, centers = i)
  silhouette_scores[i] <- mean(silhouette(km$cluster, dist(data_normalized_reduced))[, 3])
}
plot(2:15, silhouette_scores[-1], type = "b", xlab = "Número de Clusters", ylab = "Puntaje de Silhouette")

k-means

set.seed(123)

optimal_clusters_2 = 5

kmeans_model_reduced <- kmeans(data_normalized_reduced, centers = optimal_clusters_2, nstart = 25)

# Agregar etiquetas de cluster al dataset original
data_reduced$cluster <- kmeans_model_reduced$cluster
# PCA
pca_model_reduced <- prcomp(data_normalized_reduced, scale = TRUE)

# Visualización de los dos primeros componentes principales
library(ggplot2)
pca_data_reduced <- data.frame(pca_model_reduced$x[, 1:2], cluster = as.factor(data_reduced$cluster))
ggplot(pca_data_reduced, aes(x = PC1, y = PC2, color = cluster)) +
  geom_point() +
  theme_minimal() +
  labs(title = "Clustering visualizado en espacio PCA")

En este caso, se visualizan cinco clústeres, lo que podría indicar una mayor sensibilidad del modelo al agrupar los datos reducidos.

Los clústeres están más compactos en el centro del gráfico, con menos separación clara en comparación con la versión completa.

Existen clústeres adicionales (como el clúster 5, representado en rosa), lo que puede indicar una partición más forzada o sensibilidad al ruido.

Hay un mayor solapamiento entre los clústeres en comparación con el gráfico sin reducción de datos.

El rango del eje PC1 varía de -5 a 15, mientras que PC2 tiene una distribución de -5 a 15, lo que sugiere un cambio en la importancia relativa de los componentes principales después de la reducción de variables.

En conclusión, sin reducción, los clústeres son más definidos y separados, lo que sugiere que las variables eliminadas contienen información relevante para distinguir los grupos.

Con reducción, los clústeres muestran mayor solapamiento y parecen menos definidos, posiblemente debido a la pérdida de información al reducir las variables.

La aparición de un quinto clúster en el conjunto reducido indica que la reducción puede introducir sensibilidad al ruido o revelar patrones más complejos (aunque menos significativos).

LS0tDQp0aXRsZTogIlIgTm90ZWJvb2siDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQNCiAgd29yZF9kb2N1bWVudDogZGVmYXVsdA0KICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQNCiAgaHRtbF9kb2N1bWVudDoNCiAgICBkZl9wcmludDogcGFnZWQNCi0tLQ0KDQotLS0NCnRpdGxlOiAiUiBOb3RlYm9vayINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCiA8Zm9udCBzaXplPSI1Ij4gQ29uanVudG8gZGUgZGF0b3MgZGUgZGnDoWdub3N0aWNvIGRlIGPDoW5jZXIgZGUgbWFtYSBlbiBXaXNjb25zaW4uIDwvZm9udD4gDQogDQo8Zm9udCBzaXplPSI0Ij4gMS4gQW7DoWxpc2lzIEluaWNpYWwgeSBQcmVwcm9jZXNhbWllbnRvIGRlIGxvcyBEYXRvcy4gPC9mb250PiANCg0KDQo8Zm90biBzaXplPSIzIj4xLjEgSW1wb3J0YWNpw7NuIHkgQ2FyZ2EgZGUgRGF0b3MuIDwvZm9udD4gDQoNClBhcmEgY29tZW56YXIgbnVlc3RybyBhbsOhbGlzaXMgZXMgbmVjZXNhcmlvIHJlYWxpemFyIGxhIGNhcmdhIGRlIGxvcyBkYXRvczoNCg0KDQpgYGB7cn0NCg0KZGF0YSA8LSByZWFkLmNzdigiZGF0YS9kYXRhLmNzdiIpDQoNCmRhdGENCg0KYGBgDQoNCkluc3RhbGFtb3MgbG9zIHBhcXVldGVzIHF1ZSBzZXLDoW4gbmVjZXNhcmlvcyBkdXJhbnRlIG51ZXN0cm8gcHJveWVjdG86DQoNCisgVGlkeXZlcnNlOiBQYXJhIGxhIG1hbmlwdWxhY2nDs24gZGUgZGF0b3MgeSBncsOhZmljb3MuDQorIENhcmV0OiBQYXJhIGVsIHByZXByb2Nlc2FtaWVudG8geSBtb2RlbGFkbw0KICBMYXR0aWNlIGVzIHJlcXVlcmlkbyBwb3IgQ2FyZXQNCisgRGF0YUV4cGxvcmVyOiBQYXJhIGxhIGV4cGxvcmFjacOzbiBhdXRvbWF0aXphZGEgZGUgbG9zIGRhdG9zLg0KKyBEcGx5cjogUHJvcG9yY2lvbmEgdW5hIGdyYW3DoXRpY2EgZGUgbWFuaXB1bGFjacOzbiBkZSBkYXRvcy4NCisgR2dwbG90MjogUGVyc29uYWxpemFjacOzbiBkZSBncsOhZmljYXMuDQorIFBzeWNoOiBQYXJhIGFuw6FsaXNpcyBlc3RhZMOtc3RpY28NCisgQ29ycnBsb3Q6IFZpc3VhbGl6YWNpw7NuIGRlIG1hdHJpeiBkZSBjb3JyZWxhY2nDs24uDQoNCmBgYHtyfQ0KDQojaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJjYXJldCIpDQojaW5zdGFsbC5wYWNrYWdlcygiRGF0YUV4cGxvcmVyIikNCiNpbnN0YWxsLnBhY2thZ2VzKCJkcGx5ciIpDQojaW5zdGFsbC5wYWNrYWdlcygiZ2dwbG90MiIpDQojaW5zdGFsbC5wYWNrYWdlcygibGF0dGljZSIpDQojaW5zdGFsbC5wYWNrYWdlcygicHN5Y2giKQ0KI2luc3RhbGwucGFja2FnZXMoImNvcnJwbG90IikNCiNpbnN0YWxsLnBhY2thZ2VzKCJnZ2NvcnJwbG90IikNCg0KbGlicmFyeShjYXJldCkNCmxpYnJhcnkoRGF0YUV4cGxvcmVyKQ0KbGlicmFyeSgpDQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGxhdHRpY2UpDQpsaWJyYXJ5KHBzeWNoKQ0KbGlicmFyeShjb3JycGxvdCkNCmxpYnJhcnkoZ2djb3JycGxvdCkNCg0KDQoNCg0KYGBgDQoNCg0KSGFnYW1vcyB1bmEgdmlzdGEgaW5pY2lhbCBkZSBsb3MgZGF0b3M6DQpgYGB7cn0NCiNwcmltZXJhcyBmaWxhcyBkZSBudWVzdHJvIGRhdGFzZXQ6DQoNCmhlYWQoZGF0YSkNCmBgYA0KYGBge3J9DQojIERpbWVuc2nDs24gZGUgbnVlc3RybyBkYXRhc2V0Og0KDQpkaW0oZGF0YSkNCmBgYA0KTnVlc3RybyBkYXRhc2V0IGN1ZW50YSBjb24gMzMgY29sdW1uYXMgeSA1NjkgZmlsYXMuDQoNCmBgYHtyfQ0KIyBDb24gc3RyIG1vc3RyYW1vcyBsYSBlc3RydWN0dXJhIGRlIG51ZXN0cm8gZGF0YWZyYW1lLCBpbmNsdXllbmRvIGxvcyB0aXBvcyBkZSBudWVzdHJhcyB2YXJpYWJsZXM6DQoNCnN0cihkYXRhKQ0KYGBgDQpgYGB7cn0NCg0KIyBDb24gRGVzY3JpYmUgcG9kZW1vcyB2ZXIgdW4gcHJpbWVyIHJlc3VtZW4gZXN0YWTDrXN0aWNvIGLDoXNpY286DQoNCmRlc2NyaWJlKGRhdGEpDQoNCmBgYA0KDQpgYGB7cn0NCiMgVGlwb3MgZGUgZGF0b3MgZW4gbnVlc3RyYXMgdmFyaWFibGVzOg0KDQpzYXBwbHkoZGF0YSwgY2xhc3MpDQpgYGANCmBgYHtyfQ0KIyBWZWFtb3Mgc2kgZXhpc3RlbiB2YWxvcmVzIGZhbHRhbnRlcyBlbiBudWVzdHJvcyBkYXRvczoNCg0KYW55TkEoZGF0YSkNCmBgYA0KYGBge3J9DQojQ29udGFtb3MgZWwgbnVtZXJvIGRlIHZhbG9yZXMgZmFsdGFudGVzIHBvciBjb2x1bW5hOg0KDQpjb2xTdW1zKGlzLm5hKGRhdGEpKQ0KcGxvdF9taXNzaW5nKGRhdGEpDQpgYGANCkNvbW8gcHVlZGUgb2JzZXJ2YXJzZSwgY29udGFtb3MgY29uIDU2OSB2YWxvcmVzIGZhbHRhbnRlcyBlbiBsYSDDumx0aW1hIGNvbHVtbmEgIlgiLiBNw6FzIGFkZWxhbnRlIHZlcmVtb3MgY29tbyB0cmF0YXJsby4NCg0KDQo8Zm9udCBzaXplPSIzIj4xLjIgQW7DoWxpc2lzIGV4cGxvcmF0b3JpbyBkZSBsb3MgZGF0b3MgPC9mb250PiANCg0KRW4gZXN0ZSBwYXNvIG51ZXN0cm8gb2JqZXRpdm8gc2Vyw6EgZW50ZW5kZXIgbGEgZGlzdHJpYnVjacOzbiB5IHJlbGFjaW9uZXMgZGUgdmFyaWFibGVzLg0KDQo8Zm9udCBzaXplPSIyIj5WaXN1YWxpemFjacOzbiBkZSBkaXN0cmlidWNpb25lcyB5IGNvcnJlbGFjaW9uZXM6IDwvZm9udD4gDQoNCmBgYHtyfQ0KI1ZlYW1vcyB1biByZXN1bWVuIGdyw6FmaWNvIGdlbmVyYWw6DQoNCnBsb3RfaW50cm8oZGF0YSkNCmBgYA0KDQpgYGB7cn0NCiNWYXJpYWJsZXMgQ2F0ZWfDs3JpY2FzOg0KDQpwbG90X2JhcihkYXRhKQ0KYGBgDQpgYGB7cn0NCiNWYXJpYWJsZXMgTnVtw6lyaWNhcw0KcGxvdF9oaXN0b2dyYW0oZGF0YSkNCmBgYA0KDQoNCjxmb250IHNpemU9IjMiPjEuMyBQcmVwcm9jZXNhbWllbnRvIGRlIGxvcyBkYXRvcy4gPC9mb250PiANCg0KPGZvbnQgc2l6ZT0iMiI+RWxpbWluYWNpw7NuIGRlIHZhbG9yZXMgZmFsdGFudGVzOiA8L2ZvbnQ+IA0KDQoNCg0KUGFyYSBjb21lbnphciwgeWEgc2FiZW1vcyBxdWUgZXhpc3RlIHVuYSBjb2x1bW5hIGN1eW9zIHZhbG9yZXMgc29uIHRvZG9zIE5BLCBlcyBkZWNpciwgZmFsdGFudGVzLiBFbCBwcmltZXJvIHBhc28gZW4gbnVlc3RybyBwcmVwcm9jZXNhbWllbnRvIHNlcsOhIGVsaW1pbmFyIGVzdGEgY29sdW1uYSAieCI6DQoNCmBgYHtyfQ0KZGF0YSA8LSByZWFkLmNzdigiZGF0YS9kYXRhLmNzdiIpDQpkYXRhDQpoZWFkKGRhdGEpDQpgYGANCg0KYGBge3J9DQpkYXRhIDwtIGRhdGEgJT4lIHNlbGVjdCgtWCkNCmBgYA0KDQpWZWFtb3Mgc2kgc2UgaGEgYm9ycmFkbyBjb3JyZWN0YW1lbnRlIGxhIGNvbHVtbmEgWCB5IHNpIGFob3JhIGV4aXN0ZSBhbGfDum4gb3RybyB2YWxvciBmYWx0YW50ZToNCg0KYGBge3J9DQpjb2xuYW1lcyhkYXRhKQ0KYGBgDQpgYGB7cn0NCiMgVmVhbW9zIHNpIGV4aXN0ZW4gdmFsb3JlcyBmYWx0YW50ZXMgZW4gbnVlc3Ryb3MgZGF0b3M6DQoNCmFueU5BKGRhdGEpDQoNCiNDb250YW1vcyBlbCBudW1lcm8gZGUgdmFsb3JlcyBmYWx0YW50ZXMgcG9yIGNvbHVtbmE6DQoNCmNvbFN1bXMoaXMubmEoZGF0YSkpDQpwbG90X21pc3NpbmcoZGF0YSkNCmBgYA0KDQpBZGVtw6FzIGRlIGxhIGNvbHVtbmEgY29uIHZhbG9yZXMgRmFsdGFudGVzLCB0YW1iacOpbiB0ZW5lbW9zIHVuYSBjb2x1bW5hICJJRCIgcXVlIG5vIG5vcyBhcG9ydGEgbmluZ3VuYSBpbmZvcm1hY2nDs24gcG9yIGxvIHF1ZSB0YW1iacOpbiBwcm9jZWRlcmVtb3MgYSBlbGltaW5hcmxhOg0KDQpgYGB7cn0NCmRhdGEgPC0gZGF0YSAlPiUgc2VsZWN0KC1pZCkNCg0KYGBgDQoNCg0KYGBge3J9DQpjb2xuYW1lcyhkYXRhKQ0KaGVhZChkYXRhKQ0KDQpgYGANCg0KQ29tbyBwb2RlbW9zIG9ic2VydmFyLCBzZSBoYSBlbGltaW5hZG8gbGEgY29sdW1uYSAiaWQiIHkgc2UgaGEgdmVyaWZpY2FkbyBxdWUgbm8gZXhpc3RlbiBtYXMgdmFsb3JlcyBmYWx0YW50ZXMgZXhjZXB0b3MgbG9zIHlhIGVsaW1pbmFkb3MgZW4gIlgiLg0KDQpEZXNwdcOpcyBkZSBlbGxvcyBjb250YW1vcyBjb24gdW4gZGF0YXNldCBkZToNCg0KYGBge3J9DQpkaW0oZGF0YSkNCmBgYA0KNTY5IGZpbGFzIHkgMzEgY29sdW1uYXMuDQoNCjxmb250IHNpemU9IjIiPiBDb2RpZmljYWNpw7NuIGRlIHZhcmlhYmxlcyBDYXRlZ8OzcmljYXMgPC9mb250PiANCg0KDQpBbnRlcmlvcm1lbnRlIHZpbW9zIHF1ZSBudWVzdHJvIGRhdGFzZXQgY3VlbnRhIGNvbiB1bmEgw7puaWNhIGNvbHVtbmEgZGUgdmFsb3JlcyBjYXRlZ8Ozcmljb3MuICJEaWFnbm9zaXMiIGN1eW9zIHZhbG9yZXMgdGllbmVuIGVsIHNpZ3VpZW50ZSBzaWduaWZpY2FkbzoNCisgTSAobWFsaWduYW50KQ0KKyBCIChiZW5pZ24pDQoNCkVsIHNpZ3VpZW50ZSBwYXNvIGVuIGVsIHByZXByb2Nlc2FkbyBkZSBkYXRvcyBzZXLDoSBwYXNhciBlc3RhIGNvbHVtbmEgYSBudW3DqXJpY2EuIENvbW8gc29sbyBzZSBwcmVzZW50YW4gZG9zIHBvc2libGVzIHZhbG9yZXMgKE0geSBCKSwgc2UgYXBsaWNhcsOhIENvZGlmaWNhY2nDs24gQmluYXJpYTogRWwgdmFsb3IgIk0iIHBhc2Fyw6EgYSBzZXIgMSB5IHZhbG9yICJCIiBwYXNhcsOhIGEgc2VyIDAuDQoNCkF1bnF1ZSBtw6FzIGFkZWxhbnRlIHZvbHZlcmVtb3MgYSBwYXNhcmxhIGEgY2F0ZWfDs3JpY2EsIGFob3JhIGVzIG5lY2VzYXJpbyBoYWNlcmxhIGJpbmFyaWEgcGFyYSBwb2RlciBlc3R1ZGlhciBsYSBjb3JyZWxhY2nDs24gZGUgbGFzIHZhcmlhYmxlcy4NCg0KYGBge3J9DQoNCiNNIC0+IDE7IEIgLT4gMA0KZGF0YSRkaWFnbm9zaXMgPC0gaWZlbHNlKGRhdGEkZGlhZ25vc2lzID09ICJNIiwgMSwgMCkNCmBgYA0KDQpWYW1vcyBhIHZlcmlmaWNhciBxdWUgc2UgaGEgcmVhbGl6YWRvIGNvcnJlY3RhbWVudGUgbGEgY29udmVyc2nDs246DQoNCmBgYHtyfQ0KdGFibGUoZGF0YSRkaWFnbm9zaXMpDQpwcmludChkYXRhJGRpYWdub3NpcykNCmBgYA0KTm9zIGNlcmNpb3JhbW9zIGRlIHF1ZSBubyBleGlzdGUgbmluZ8O6biBvdHJvIHZhbG9yIGNhdGVnw7NyaWNvIGVuIGVsIGRhdGFzZXQ6DQpgYGB7cn0NCg0KY2F0ZWdvcmljYWxfY29sdW1ucyA8LSBzYXBwbHkoZGF0YSwgaXMuZmFjdG9yKSB8IHNhcHBseShkYXRhLCBpcy5jaGFyYWN0ZXIpDQpuYW1lcyhkYXRhKVtjYXRlZ29yaWNhbF9jb2x1bW5zXQ0KDQoNCmBgYA0KRWZlY3RpdmFtZW50ZSwgdG9kYXMgbnVlc3RyYXMgY29sdW1uYXMgYWhvcmEgc29uIG51bcOpcmljYXMsIGxvIHF1ZSBub3MgZGEgcGFzbyBhbCBzaWd1aWVudGUgcHVudG8gZW4gbnVlc3RybyBwcmVwcm9jZXNhbWllbnRvIGRlIGRhdG9zLg0KDQo8Zm9udCBzaXplPSIyIj4gRXN0dWRpbyBkZSBjb3JyZWxhY2nDs24uIDwvZm9udD4gDQoNCg0KQWwgdGVuZXIgbnVlc3RybyBkYXRhc2V0IGxpbXBpbyBkZSBOQSB5IHNvbG8gcHJlc2VudGVzIHZhcmlhYmxlcyBudW3DqXJpY2FzLCAgZWwgc2lndWllbnRlIHBhc28gc2Vyw6EgZXN0dWRpYXIgbGFzIHBvc2libGVzIGNvcnJlbGFjaW9uZXMgZGUgbnVlc3RybyBkYXRhc2V0Lg0KDQpFc3R1ZGlhciBsYSBjb3JyZWxhY2nDs24gZGUgbG9zIGRhdG9zIGF5dWRhIGEgaWRlbnRpZmljYXIgcGF0cm9uZXMgeSByZWxhY2lvbmVzIGVudHJlIHZhcmlhYmxlcywgbG8gcXVlIHBvZHLDrWEgY29uZHVjaXIgYSBudWV2YXMgaGlww7N0ZXNpcyB5IGRlc2N1YnJpbWllbnRvcy4gQWRlbcOhcywgdW4gYnVlbiBlc3R1ZGlvIGRlIGNvcnJlbGFjaW9uZXMgcG9kcsOtYSBzZXIgw7p0aWwgcGFyYSBzZWxlY2Npb25hciB2YXJpYWJsZXMgcmVsZXZhbnRlcyB5IGNvbnN0cnVpciBtb2RlbG9zIGVuIHVuIGZ1dHVyby4NCg0KDQpMb3MgdmFsb3JlcyBxdWUgb2J0ZW5kcmVtb3MgdGVuZHLDoW4gbGEgc2lndWllbnRlIGludGVycHJldGFjacOzbjoNCg0KKyBDb3JyZWxhY2nDs24gY2VyY2FuYSBhIDE6IFJlbGFjacOzbiBwb3NpdGl2YSBmdWVydGUuDQorIENvcnJlbGFjacOzbiBjZXJjYW5hIGEgLTE6IFJlbGFjacOzbiBuZWdhdGl2YSBmdWVydGUuDQorIENvcnJlbGFjacOzbiBjZXJjYW5hIGEgMDogTm8gaGF5IHJlbGFjacOzbiBsaW5lYWwgc2lnbmlmaWNhdGl2YQ0KDQpgYGB7cn0NCg0KIyBDYWxjdWxhciBtYXRyaXogZGUgY29ycmVsYWNpw7NuDQpjb3JyZWxhdGlvbl9tYXRyaXggPC0gY29yKGRhdGEsIHVzZSA9ICJjb21wbGV0ZS5vYnMiKSAgIyBJZ25vcmEgdmFsb3JlcyBmYWx0YW50ZXMNCg0KDQojIEdyYWZpY2FyIG1hdHJpeiBkZSBjb3JyZWxhY2nDs24NCmNvcnJwbG90KGNvcnJlbGF0aW9uX21hdHJpeCwgbWV0aG9kID0gImNvbG9yIiwgdHlwZSA9ICJ1cHBlciIsIHRsLmNleCA9IDAuOCkNCg0KYGBgDQpgYGB7cn0NCg0KIyBHcmFmaWNhciBsYSBtYXRyaXogZGUgY29ycmVsYWNpw7NuIGNvbiB2YWxvcmVzIGVuIGxhcyBjZWxkYXMNCmdnY29ycnBsb3QoY29ycmVsYXRpb25fbWF0cml4LCANCiAgICAgICAgICAgbWV0aG9kID0gImNpcmNsZSIsICAjIFV0aWxpemFyIGPDrXJjdWxvcyBwYXJhIHJlcHJlc2VudGFyIGNvcnJlbGFjaW9uZXMNCiAgICAgICAgICAgdHlwZSA9ICJsb3dlciIsICAgICAjIE1vc3RyYXIgc29sbyBsYSBtaXRhZCBpbmZlcmlvcg0KICAgICAgICAgICBsYWIgPSBUUlVFLCAgICAgICAgICMgQcOxYWRpciBsb3MgdmFsb3JlcyBkZSBjb3JyZWxhY2nDs24NCiAgICAgICAgICAgbGFiX3NpemUgPSAyLCAgICAgICAjIFRhbWHDsW8gZGVsIHRleHRvIGVuIGxhcyBjZWxkYXMNCiAgICAgICAgICAgY29sb3JzID0gYygiYmx1ZSIsICJ3aGl0ZSIsICJyZWQiKSwgIyBDb2xvcmVzIHBhcmEgbGFzIGNvcnJlbGFjaW9uZXMgbmVnYXRpdmFzLCBuZXV0cmFsZXMgeSBwb3NpdGl2YXMNCiAgICAgICAgICAgdGl0bGUgPSAiTWF0cml6IGRlIENvcnJlbGFjacOzbiIsICAjIFTDrXR1bG8gZGVsIGdyw6FmaWNvDQogICAgICAgICAgIHRsLmNleCA9IDEwKSAgICAgICAjIFRhbWHDsW8gZGUgbGFzIGV0aXF1ZXRhcw0KDQpgYGANCkNvbW8gcHVlZGUgb2JzZXJ2YXJzZSwgYWwgdHJhdGFyZXNlIHVuIGNvbmp1bnRvIGRlIGRhdG9zIGNvbiAzMSB2YXJpYWJsZXMsIGxhIG1hdHJpeiBkZSBjb3JyZWxhY2nDs24gY3Vlc3RhIGludGVycHJldGFybGEuDQoNClBvciBlbGxvLCBjb21vICJkaWFnbm9zaXMiIGVzIG51ZXN0cmEgdmFyaWFibGUgb2JqZXRpdm8sIHZhbW9zIGEgb2JzZXJ2YXIgY8OzbW8gc2UgY29ycmVsYWNpb25hbiBsYXMgZGVtw6FzIHZhcmlhYmxlcyBjb24gZWxsYToNCg0KDQpgYGB7cn0NCiMgQ2FsY3VsYXIgbGEgY29ycmVsYWNpw7NuIGVudHJlIGNhZGEgdmFyaWFibGUgbnVtw6lyaWNhIHkgJ2RpYWdub3NpcycNCmNvcl93aXRoX3RhcmdldCA8LSBjb3IoZGF0YSwgZGF0YSRkaWFnbm9zaXMsIHVzZSA9ICJjb21wbGV0ZS5vYnMiKQ0KDQojIENyZWFyIHVuIGRhdGEgZnJhbWUgcGFyYSB2ZXIgbGFzIGNvcnJlbGFjaW9uZXMganVudG8gY29uIGxvcyBub21icmVzIGRlIGxhcyB2YXJpYWJsZXMNCmNvcnJlbGF0aW9uX2RmIDwtIGRhdGEuZnJhbWUoVmFyaWFibGUgPSBuYW1lcyhkYXRhKSwgQ29ycmVsYXRpb24gPSBjb3Jfd2l0aF90YXJnZXQpDQoNCiMgT3JkZW5hciBlbCBkYXRhIGZyYW1lIHBvciBsYSBjb2x1bW5hIGRlIENvcnJlbGF0aW9uIGVuIG9yZGVuIGRlc2NlbmRlbnRlDQpjb3JyZWxhdGlvbl9kZl9zb3J0ZWQgPC0gY29ycmVsYXRpb25fZGZbb3JkZXIoLWNvcnJlbGF0aW9uX2RmJENvcnJlbGF0aW9uKSwgXQ0KDQojIFZlciBsYXMgY29ycmVsYWNpb25lcyBvcmRlbmFkYXMganVudG8gY29uIGxvcyBub21icmVzIGRlIGxhcyB2YXJpYWJsZXMNCnByaW50KGNvcnJlbGF0aW9uX2RmX3NvcnRlZCkNCg0KDQpgYGANCkVuIGVzdGEgdGFibGEgcG9kZW1vcyB2ZXIgbGEgY29ycmVsYWNpw7NuIGRlIGNhZGEgdmFyaWFibGUgY29uICJkaWFnbm9zaXMiIGVuIG9yZGVuIGRlc2NlbmRlbnRlLg0KDQoNCjxmb250IHNpemU9IjIiPlNlbGVjY2nDs24gZGUgQXRyaWJ1dG9zOiA8L2ZvbnQ+IA0KDQpVbmEgZGUgbGFzIGZvcm1hcyBkZSBoYWNlciB1bmEgY29ycmVjdGEgc2VsZWNjacOzbiBkZSBhdHJpYnV0b3MgZXMgaW5zcGVjY2lvbmFyIGxhIGFudGVyaW9yIHRhYmxhIGRlIGNvcnJlbGFjacOzbjoNCg0KQ29tbyBwdWVkZSBvYnNlcnZhcnNlLCBubyBleGlzdGUgbmluZ3VuYSB2YXJpYWJsZSBxdWUgc3VwZXJlIGVsIDAsOCwgY3VhbmRvIGxhcyB2YXJpYWJsZXMgc2UgY29ycmVsYWNpb25hbiBjb24gbGEgdmFyaWFibGUgY2xhc2UsIGVuIG51ZXN0cm8gY2FzbyBkaWFnbm9zaXMsIGNvbiBtw6FzIGRlIHVuIDAsOSwgc3VlbGUgc2VyIGNvbnZlbmllbnRlIGVsaW1pbmFybGFzIHBhcmEgZXZpdGFyIHJlZHVuZGFuY2lhLCBsbyBxdWUgbm8gZXMgbnVlc3RybyBjYXNvLiANCg0KUG9yIG90cm8gbGFkbywgdGFtcG9jbyB0ZW5lbW9zIHZhbG9yZXMgZGVtYXNpYWRvIGJham9zIG5pIG5lZ2F0aXZvcywgbG9zIHZhbG9yZXMgbmVnYXRpdm9zIGVuIG51ZXN0cm8gY2FzbywgcG9kcsOtYW4gYXl1ZGFyIGFsIGVudHJlbmFtaWVudG8gZGUgbnVlc3RybyBtb2RlbG8sIGVzIHBvciBlbGxvLCBxdWUgZGUgbW9tZW50byBubyBzZSBlbGltaW5hcsOhbiB2YXJpYWJsZXMgZGVsIGRhdGFzZXQuIA0KDQo8Zm9udCBzaXplPSI0Ij4gMi4gQW7DoWxpc2lzIFN1cGVydmlzYWRvLjwvZm9udD4gDQoNCkVsIGFwcmVuZGl6YWplIHN1cGVydmlzYWRvLCB0YW1iacOpbiBjb25vY2lkbyBjb21vIG1hY2hpbmUgbGVhcm5pbmcgc3VwZXJ2aXNhZG8sIGVzIHVuYSBzdWJjYXRlZ29yw61hIGRlbCBtYWNoaW5lIGxlYXJuaW5nIHkgbGEgaW50ZWxpZ2VuY2lhIGFydGlmaWNpYWwuIFNlIGRlZmluZSBwb3IgZWwgdXNvIGRlIGNvbmp1bnRvcyBkZSBkYXRvcyBldGlxdWV0YWRvcyBwYXJhIGVudHJlbmFyIGFsZ29yaXRtb3MgcXVlIGNsYXNpZmljYW4gbG9zIGRhdG9zIG8gcHJlZGljZW4gbG9zIHJlc3VsdGFkb3MgY29uIHByZWNpc2nDs24uDQoNCkEgbWVkaWRhIHF1ZSBzZSBpbnRyb2R1Y2VuIGRhdG9zIGVuIGVsIG1vZGVsbywgw6lzdGUgYWp1c3RhIHN1cyBwb25kZXJhY2lvbmVzIGhhc3RhIHF1ZSBlbCBtb2RlbG8gc2UgaGEgYWp1c3RhZG8gYWRlY3VhZGFtZW50ZSwgbG8gcXVlIG9jdXJyZSBjb21vIHBhcnRlIGRlbCBwcm9jZXNvIGRlIHZhbGlkYWNpw7NuIGNydXphZGEuIA0KDQpFbiBudWVzdHJvIGFuw6FsaXNpcywgbGEgdmFyaWFibGUgYSBwcmVkZWNpciBzZXLDoSAiZGlhZ25vc2lzIiwgcXVlIGNvbW8gdmltb3MgYW50ZXJpb3JtZW50ZSwgZXMgZGUgdGlwbyBiaW5hcmlvLCB0b21hbmRvIDEgY3VhbmRvIGVsIHR1bW9yIGVzIG1hbGlnbm8sIHkgMCBjdWFuZG8gZWwgdHVtb3IgcmVzdWx0YSBiZW5pZ25vLg0KDQpFbiBlc3RlIGFuw6FsaXNpcyBzZSBoYW4gZXZhbHVhZG8gZGlmZXJlbnRlcyBtb2RlbG9zIGRlIGNsYXNpZmljYWNpw7NuIHBhcmEgdW4gY29uanVudG8gZGUgZGF0b3MgYmluYXJpbyBjb24gNTY5IG11ZXN0cmFzIHkgMzAgcHJlZGljdG9yZXMsIGNsYXNpZmljYWRvcyBlbiBkb3MgY2F0ZWdvcsOtYXM6ICdOZWdhdGl2ZScgeSAnUG9zaXRpdmUnLiBMb3MgbW9kZWxvcyBhbmFsaXphZG9zIHNvbiBlbCDDgXJib2wgZGUgRGVjaXNpw7NuIChDQVJUKSwgUmFuZG9tIEZvcmVzdCAoQm9zcXVlIEFsZWF0b3JpbyksIE3DoXF1aW5hcyBkZSBTb3BvcnRlIFZlY3RvcmlhbCBjb24gbsO6Y2xlbyBSYWRpYWwgKFNWTSksIHkgZWwgTW9kZWxvIExpbmVhbCBHZW5lcmFsaXphZG8gKEdMTSkuDQoNCkVsIHByaW1lciBwYXNvIHBhcmEgZWwgYW7DoWxpc2lzIHN1cGVydmlzYWRvIGVzIHJlYWxpemFyIGxhIGRpdmnDs24gZGVsIGRhdGFzZXQuDQoNCjxmb250IHNpemU9IjIiPjIuMSBEaXZpc2nDs24gZGVsIERhdGFzZXQ6IDwvZm9udD4gDQoNCg0KDQoNClBhcmEgZXN0ZSBwYXNvLCB1c2FyZW1vcyBsYSB2YWxpZGFjacOzbiBjcnV6YWRhIGstZm9sZCBwYXJhIGRpdmlkaXIgZWwgZGF0YXNldCB5IGV2YWx1YXIgZWwgbW9kZWxvLiBBdW5xdWUgcG9kcsOtYW1vcyBkaXZpZGlyIGVsIGNvbmp1bnRvIGVuIHVuIDgwJSB0cmFpbiB5IHVuIDIwJSB0cmFpbiwgdmFsb3JlcyB0w61waWNvcywgc2UgaGEgb3B0YWRvIHBvciB1dGlsaXphciBLLWZvbGQgeWEgcXVlIGFzZWd1cmEgcXVlIGNhZGEgc3ViY29uanVudG8gbyBmb2xkIGRlbCBkYXRhc2V0IGVzIHV0aWxpemFkbyB0YW50byBwYXJhIGVsIGVudHJlbmFtaWVudG8gY29tbyBwYXJhIHBydWViYSBlbiBkaXN0aW50YXMgaXRlcmFjaW9uZXMsIGxvIHF1ZSBub3MgcHJvcG9yY2lvbmFyw6EgdW5hIGV2YWx1YWNpw7NuIG3DoXMgcm9idXN0YS4NCg0KYGBge3J9DQojQ2FyZ2Ftb3MgbGEgbGlicmVyw61hIGNhcmV0DQpsaWJyYXJ5KGNhcmV0KQ0KDQojQ29uZmlndXJhbW9zIGxhIHZhbGlkYWNpw7NuIGNydXphZGEgSy1mb2xkDQpzZXQuc2VlZCgxMjMpDQoNCmsgPC0gNSANCg0KIyBDb25maWd1cmFyIEstZm9sZCBDcm9zcy1WYWxpZGF0aW9uIGNvbiBwcm9iYWJpbGlkYWRlcyBkZSBjbGFzZQ0KdHJhaW5fY29udHJvbCA8LSB0cmFpbkNvbnRyb2woDQogIG1ldGhvZCA9ICJjdiIsICAgICAgICAgICAjIENyb3NzLXZhbGlkYXRpb24NCiAgbnVtYmVyID0gaywgICAgICAgICAgICAgICMgTsO6bWVybyBkZSBwbGllZ3VlcyAoZm9sZHMpDQogIGNsYXNzUHJvYnMgPSBUUlVFLCAgICAgICAjIEhhYmlsaXRhciBwcm9iYWJpbGlkYWRlcyBkZSBjbGFzZQ0KICBzdW1tYXJ5RnVuY3Rpb24gPSB0d29DbGFzc1N1bW1hcnksICMgUGFyYSBtw6l0cmljYXMgZGUgY2xhc2lmaWNhY2nDs24gYmluYXJpYQ0KICBzYXZlUHJlZGljdGlvbnMgPSAiZmluYWwiICMgR3VhcmRhciBsYXMgcHJlZGljY2lvbmVzIGZpbmFsZXMNCikNCg0KDQpgYGANCg0KDQoNCjxmb250IHNpemU9IjIiPjIuMiBFbnRyZW5hbWllbnRvIGRlbCBtb2RlbG86IDwvZm9udD4gDQoNCiAgDQogIENhbWJpYW1vcyBsb3Mgbml2ZWxlcyBkZWwgZmFjdG9yICJkaWFnbm9zaXMiIGEgbm9tYnJlcyB2w6FsaWRvcywgZW4gZXN0ZSBjYXNvICJNIiBwYXJhIGxvcyB0dW1vcmVzIG1hbGlnbm9zLCAiQiIgcGFyYSBhcXVlbGxvcyBiZW5pZ25vcy4NCmBgYHtyfQ0KIyBFbnRyZW5hciBlbCBtb2RlbG8gZGUgUmFuZG9tIEZvcmVzdCB1dGlsaXphbmRvIEstZm9sZCBjcm9zcy12YWxpZGF0aW9uDQpzZXQuc2VlZCgxMjMpDQoNCiNWYW1vcyBhIGNvbnZlcnRpcg0KZGF0YSRkaWFnbm9zaXMgPC0gZmFjdG9yKGRhdGEkZGlhZ25vc2lzLCBsZXZlbHMgPSBjKDAsIDEpLCBsYWJlbHMgPSBjKCJCIiwgIk0iKSkNCmBgYA0KDQoNCiAgPGZvbnQgc2l6ZT0iMiI+Mi4yLjEgw4FyYm9sIGRlIERlY2lzacOzbiAoQ0FSVCk6IDwvZm9udD4gDQogIA0KICBFbCDDoXJib2wgZGUgZGVjaXNpw7NuIGVzIHVuIG1vZGVsbyBzaW1wbGUgcXVlIGRpdmlkZSBsb3MgZGF0b3MgZW4gc2VnbWVudG9zIGJhc2Fkb3MgZW4gcmVnbGFzIGRlIGRlY2lzacOzbi4gRXMgw7p0aWwgcGFyYSBjbGFzaWZpY2FjaW9uZXMgZG9uZGUgbGFzIGRlY2lzaW9uZXMgc29uIGzDs2dpY2FzIHkgZsOhY2lsZXMgZGUgZW50ZW5kZXINCiAgDQpgYGB7cn0NCg0KIyDDgXJib2wgZGUgRGVjaXNpw7NuIHV0aWxpemFuZG8gSy1mb2xkIGNyb3NzLXZhbGlkYXRpb24NCm1vZGVsX2NhcnQgPC0gdHJhaW4oDQogIGRpYWdub3NpcyB+IC4sICAgICAgICAjIFVzYW1vcyB0b2RhcyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzDQogIGRhdGEgPSBkYXRhLA0KICBtZXRob2QgPSAicnBhcnQiLCAgICAgIyDDgXJib2wgZGUgZGVjaXNpw7NuIChDQVJUKQ0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX2NhcnQpDQoNCiMgRXh0cmFlciBsYSBpbXBvcnRhbmNpYSBkZSBsYXMgdmFyaWFibGVzDQppbXBvcnRhbmNlX2NhcnQgPC0gdmFySW1wKG1vZGVsX2NhcnQsIHNjYWxlID0gRkFMU0UpDQppbXBvcnRhbmNlX2NhcnRfZGYgPC0gaW1wb3J0YW5jZV9jYXJ0JGltcG9ydGFuY2UNCmltcG9ydGFuY2VfY2FydF9kZiR2YXJpYWJsZSA8LSByb3duYW1lcyhpbXBvcnRhbmNlX2NhcnRfZGYpDQoNCmBgYA0KRWwgcHJpbWVyIG1vZGVsbyBhbmFsaXphZG8gZXMgZWwgw4FyYm9sIGRlIERlY2lzacOzbiAoQ0FSVCkuIEVzdGUgbW9kZWxvIHV0aWxpemEgdW4gdmFsb3IgZGUgY29tcGxlamlkYWQgZGUgcG9kYSAoY3ApIGRlIDAuMDA0NywgcXVlIGZ1ZSBzZWxlY2Npb25hZG8gY29tbyBlbCBtZWpvciBwYXLDoW1ldHJvIG1lZGlhbnRlIHZhbGlkYWNpw7NuIGNydXphZGEuDQoNCkxhIGN1cnZhIFJPQyBkZWwgbW9kZWxvIGVzIGRlIDAuOTM2MiwgbG8gcXVlIGluZGljYSB1bmEgYWx0YSBjYXBhY2lkYWQgcGFyYSBkaXNjcmltaW5hciBlbnRyZSBsYXMgZG9zIGNsYXNlcy4gDQoNCkxhIHNlbnNpYmlsaWRhZCAoY2FwYWNpZGFkIGRlbCBtb2RlbG8gcGFyYSBpZGVudGlmaWNhciBjb3JyZWN0YW1lbnRlIGxhcyBvYnNlcnZhY2lvbmVzIHBvc2l0aXZhcykgZXMgZGUgMC45MzgzLCBsbyBxdWUgc3VnaWVyZSBxdWUgZWwgbW9kZWxvIGVzIG11eSBlZmljaWVudGUgcGFyYSBkZXRlY3RhciBsYXMgb2JzZXJ2YWNpb25lcyBwb3NpdGl2YXMsIGF1bnF1ZSBhbGdvIG1lbm9zIGVmZWN0aXZvIHF1ZSBvdHJvcyBtb2RlbG9zIGVuIGN1YW50byBhIGxhIGVzcGVjaWZpY2lkYWQuIERlIGhlY2hvLCBsYSBlc3BlY2lmaWNpZGFkIChjYXBhY2lkYWQgcGFyYSBpZGVudGlmaWNhciBjb3JyZWN0YW1lbnRlIGxhcyBvYnNlcnZhY2lvbmVzIG5lZ2F0aXZhcykgZXMgZGUgMC44OTYwLCBsbyBxdWUgcmVwcmVzZW50YSB1bmEgbGV2ZSBjYcOtZGEgcmVzcGVjdG8gYSBsYSBzZW5zaWJpbGlkYWQuIA0KDQpFc3RlIGRlc2VtcGXDsW8gZXMgc8OzbGlkbyB5IGVxdWlsaWJyYWRvLCBwZXJvIG5vIGVzIGVsIG3DoXMgYWx0byBlbnRyZSBsb3MgbW9kZWxvcyBldmFsdWFkb3MuDQoNCiAgICA8Zm9udCBzaXplPSIyIj4yLjIuMiBSYW5kb20gRm9yZXN0IDwvZm9udD4gDQoNCkVsIFJhbmRvbSBGb3Jlc3QgZXMgdW4gYWxnb3JpdG1vIHF1ZSBjb25zdHJ1eWUgbcO6bHRpcGxlcyDDoXJib2xlcyBkZSBkZWNpc2nDs24geSByZWFsaXphIHVuYSBwcmVkaWNjacOzbiBhZ3JlZ2FuZG8gbGFzIHByZWRpY2Npb25lcyBkZSB0b2RvcyBsb3Mgw6FyYm9sZXMgaW5kaXZpZHVhbGVzLiBFcyByb2J1c3RvIGFudGUgZWwgc29icmVhanVzdGUuDQpgYGB7cn0NCm1vZGVsX3JmIDwtIHRyYWluKA0KICBkaWFnbm9zaXMgfiAuLCAgICAgICAgIyBVc2Ftb3MgdG9kYXMgbGFzIHZhcmlhYmxlcyBwcmVkaWN0b3Jhcw0KICBkYXRhID0gZGF0YSwNCiAgbWV0aG9kID0gInJmIiwgICAgICAgICMgUmFuZG9tIEZvcmVzdA0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX3JmKQ0KDQojIEV4dHJhZXIgbGEgaW1wb3J0YW5jaWEgZGUgbGFzIHZhcmlhYmxlcw0KaW1wb3J0YW5jZV9yZiA8LSB2YXJJbXAobW9kZWxfcmYsIHNjYWxlID0gRkFMU0UpDQppbXBvcnRhbmNlX3JmX2RmIDwtIGltcG9ydGFuY2VfcmYkaW1wb3J0YW5jZQ0KaW1wb3J0YW5jZV9yZl9kZiR2YXJpYWJsZSA8LSByb3duYW1lcyhpbXBvcnRhbmNlX3JmX2RmKQ0KDQpgYGANCiAgDQpQYXJhIGVudGVuZGVyIG1lam9yIGxvcyByZXN1bHRhZG9zIGRlbCBtb2RlbG8sIGFjbGFyYXIgcXVlIGVsIHBhcsOhbWV0cm8gbXRyeSBlbiBlbCBjb250ZXh0byBkZSBSYW5kb20gRm9yZXN0IGVzIHVubyBkZSBsb3MgaGlwZXJwYXLDoW1ldHJvcyBjbGF2ZSBxdWUgc2UgdXRpbGl6YSBwYXJhIGNvbnRyb2xhciBlbCBuw7ptZXJvIGRlIHZhcmlhYmxlcyAoY2FyYWN0ZXLDrXN0aWNhcykgcXVlIGVsIG1vZGVsbyBjb25zaWRlcmEgcGFyYSBkaXZpZGlyIGNhZGEgbm9kbyBlbiBjYWRhIMOhcmJvbCBkZWwgYm9zcXVlLiBFc3BlY8OtZmljYW1lbnRlLCBtdHJ5IGRlZmluZSBjdcOhbnRhcyBjYXJhY3RlcsOtc3RpY2FzIHNlcsOhbiBlbGVnaWRhcyBhbGVhdG9yaWFtZW50ZSBwYXJhIGNhZGEgbm9kbyBjdWFuZG8gc2UgY29uc3RydXllIHVuIMOhcmJvbCBlbiBlbCBSYW5kb20gRm9yZXN0Lg0KIA0KIA0KUmFuZG9tIEZvcmVzdCwgbXVlc3RyYSB1biBkZXNlbXBlw7FvIGRlc3RhY2FibGUuIEVzdGUgbW9kZWxvIHNlbGVjY2lvbsOzIGVsIHZhbG9yIGRlIG10cnkgKG7Dum1lcm8gZGUgdmFyaWFibGVzIGFsZWF0b3JpYXMgcGFyYSBjYWRhIGRpdmlzacOzbiBkZWwgw6FyYm9sKSBpZ3VhbCBhIDIsIGxvIHF1ZSBvcHRpbWl6YSBsYSBjYXBhY2lkYWQgZGUgZGlzY3JpbWluYWNpw7NuLiBTdSBjdXJ2YSBST0MgYWxjYW56YSB1biB2YWxvciBpbXByZXNpb25hbnRlIGRlIDAuOTkwOCwgbG8gcXVlIGVzIHVuIGluZGljYWRvciBjbGFybyBkZSBzdSBjYXBhY2lkYWQgcGFyYSBzZXBhcmFyIGxhcyBkb3MgY2xhc2VzIGNvbiBncmFuIHByZWNpc2nDs24uIEFkZW3DoXMsIGxhIHNlbnNpYmlsaWRhZCBkZSAwLjk4MDQgbXVlc3RyYSBxdWUgUmFuZG9tIEZvcmVzdCB0aWVuZSB1bmEgZXhjZWxlbnRlIGNhcGFjaWRhZCBwYXJhIGRldGVjdGFyIGNvcnJlY3RhbWVudGUgbGFzIG9ic2VydmFjaW9uZXMgcG9zaXRpdmFzLCB5IGxhIGVzcGVjaWZpY2lkYWQgZGUgMC45MjQxIGluZGljYSBxdWUgdGFtYmnDqW4gZXMgZWZpY2F6IGVuIGlkZW50aWZpY2FyIGxhcyBvYnNlcnZhY2lvbmVzIG5lZ2F0aXZhcy4gRXN0ZSBtb2RlbG8gc29icmVzYWxlIHBvciBzdSBhbHRhIHByZWNpc2nDs24gZW4gYW1ib3MgYXNwZWN0b3MsIGxvIHF1ZSBsbyBjb252aWVydGUgZW4gdW5vIGRlIGxvcyBtb2RlbG9zIG3DoXMgcm9idXN0b3MgeSBjb25maWFibGVzIHBhcmEgZXN0ZSBjb25qdW50byBkZSBkYXRvcy4NCg0KICA8Zm9udCBzaXplPSIyIj4yLjIuMyBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIChTVk0pIDwvZm9udD4gDQogIA0KICBFbCBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIChTVk0pIGVzIHVuIGFsZ29yaXRtbyBxdWUgaW50ZW50YSBlbmNvbnRyYXIgdW4gaGlwZXJwbGFubyBxdWUgbWVqb3Igc2VwYXJlIGxhcyBkaWZlcmVudGVzIGNsYXNlcyBkZSBkYXRvcy4NCiAgDQoNCmBgYHtyfQ0KI2luc3RhbGwucGFja2FnZXMoImtlcm5sYWIiKQ0KbGlicmFyeShrZXJubGFiKQ0KDQptb2RlbF9zdm0gPC0gdHJhaW4oDQogIGRpYWdub3NpcyB+IC4sICAgICAgICAjIFVzYW1vcyB0b2RhcyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzDQogIGRhdGEgPSBkYXRhLA0KICBtZXRob2QgPSAic3ZtUmFkaWFsIiwgICMgU3VwcG9ydCBWZWN0b3IgTWFjaGluZSBjb24ga2VybmVsIHJhZGlhbA0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX3N2bSkNCg0KIyBFeHRyYWVyIGxhIGltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXMNCmltcG9ydGFuY2Vfc3ZtIDwtIHZhckltcChtb2RlbF9zdm0sIHNjYWxlID0gRkFMU0UpDQppbXBvcnRhbmNlX3N2bV9kZiA8LSBpbXBvcnRhbmNlX3N2bSRpbXBvcnRhbmNlDQppbXBvcnRhbmNlX3N2bV9kZiR2YXJpYWJsZSA8LSByb3duYW1lcyhpbXBvcnRhbmNlX3N2bV9kZikNCg0KYGBgDQpFbCB0ZXJjZXIgbW9kZWxvIGV2YWx1YWRvIGVzIGVsIGRlIE3DoXF1aW5hcyBkZSBTb3BvcnRlIFZlY3RvcmlhbCBjb24gbsO6Y2xlbyBSYWRpYWwgKFNWTSkuIEVzdGUgbW9kZWxvLCBjb24gdW4gcGFyw6FtZXRybyBkZSByZWd1bGFyaXphY2nDs24g8J2Qtj0xIHkgdW4gdmFsb3IgZGUgc2lnbWEgZGUgMC4wNDc1LCBtb3N0csOzIHVuIGRlc2VtcGXDsW8gZXhjZWxlbnRlIGVuIHTDqXJtaW5vcyBkZSBsYSBjdXJ2YSBST0MsIGFsY2FuemFuZG8gdW4gdmFsb3IgZGUgMC45OTQ4LCBlbCBtw6FzIGFsdG8gZW50cmUgdG9kb3MgbG9zIG1vZGVsb3MuIEVzdGEgbcOpdHJpY2EgcmVmbGVqYSB1bmEgY2FwYWNpZGFkIGRlIGRpc2NyaW1pbmFjacOzbiBzdXBlcmlvciwgbG8gcXVlIGltcGxpY2EgcXVlIGVsIG1vZGVsbyB0aWVuZSB1bmEgYWx0YSBoYWJpbGlkYWQgcGFyYSBzZXBhcmFyIGNvcnJlY3RhbWVudGUgbGFzIGNsYXNlcyAnTmVnYXRpdmUnIHkgJ1Bvc2l0aXZlJy4gTGEgc2Vuc2liaWxpZGFkIGRlIDAuOTc0OCB5IGxhIGVzcGVjaWZpY2lkYWQgZGUgMC45NjcwIHRhbWJpw6luIHNvbiBub3RhYmxlbWVudGUgYWx0YXMsIGxvIHF1ZSBzdWdpZXJlIHF1ZSBlbCBtb2RlbG8gdGllbmUgdW4gYnVlbiByZW5kaW1pZW50byB0YW50byBlbiBsYSBkZXRlY2Npw7NuIGRlIGxhcyBvYnNlcnZhY2lvbmVzIHBvc2l0aXZhcyBjb21vIGVuIGxhIGNvcnJlY3RhIGlkZW50aWZpY2FjacOzbiBkZSBsYXMgbmVnYXRpdmFzLiBTaW4gZW1iYXJnbywgZXMgaW1wb3J0YW50ZSBkZXN0YWNhciBxdWUgZWwgbW9kZWxvIFNWTSBwcmVzZW50w7MgdmFyaWFzIGFkdmVydGVuY2lhcyBkdXJhbnRlIGVsIHByb2Nlc28gZGUgb3B0aW1pemFjacOzbiAod2FybmluZ3MpLCByZWxhY2lvbmFkYXMgY29uIHByb2JsZW1hcyBkZSBjb252ZXJnZW5jaWEgeSBwcm9iYWJpbGlkYWRlcyBleHRyZW1hcyBkZSAwIG8gMS4gRXN0byBwb2Ryw61hIGluZGljYXIgcXVlIGVsIG1vZGVsbyBwb2Ryw61hIGVzdGFyIHNvYnJlYWp1c3RhbmRvIG8gZW5mcmVudGFuZG8gZGlmaWN1bHRhZGVzIHBhcmEgZW5jb250cmFyIHVuIGVxdWlsaWJyaW8gZXN0YWJsZSwgbG8gcXVlIGRlYmUgdGVuZXJzZSBlbiBjdWVudGEgYWwgZXZhbHVhciBzdSBlc3RhYmlsaWRhZCB5IGdlbmVyYWxpemFjacOzbi4NCg0KDQogIDxmb250IHNpemU9IjIiPjIuMi40IFJlZ3Jlc2nDs24gTG9nw61zdGljYSA8L2ZvbnQ+IA0KICANCiAgRXN0ZSBhbGdvcml0bW8gZXMgdW4gbW9kZWxvIGRlIGNsYXNpZmljYWNpw7NuIHF1ZSBwcmVkaWNlIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgdW5hIG9ic2VydmFjacOzbiBwZXJ0ZW56Y2EgYSB1bmEgY2xhc2UgbyBuby4NCiAgDQpgYGB7cn0NCg0KDQptb2RlbF9sb2dpdCA8LSB0cmFpbigNCiAgZGlhZ25vc2lzIH4gLiwgICAgICAgICMgVXNhbW9zIHRvZGFzIGxhcyB2YXJpYWJsZXMgcHJlZGljdG9yYXMNCiAgZGF0YSA9IGRhdGEsDQogIG1ldGhvZCA9ICJnbG0iLCAgICAgICAjIFJlZ3Jlc2nDs24gTG9nw61zdGljYQ0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX2xvZ2l0KQ0KDQojIEV4dHJhZXIgbGEgaW1wb3J0YW5jaWEgZGUgbGFzIHZhcmlhYmxlcw0KaW1wb3J0YW5jZV9sb2dpdCA8LSB2YXJJbXAobW9kZWxfbG9naXQsIHNjYWxlID0gRkFMU0UpDQppbXBvcnRhbmNlX2xvZ2l0X2RmIDwtIGltcG9ydGFuY2VfbG9naXQkaW1wb3J0YW5jZQ0KaW1wb3J0YW5jZV9sb2dpdF9kZiR2YXJpYWJsZSA8LSByb3duYW1lcyhpbXBvcnRhbmNlX2xvZ2l0X2RmKQ0KDQpgYGANCkVsIMO6bHRpbW8gbW9kZWxvIGNvbnNpZGVyYWRvIGVzIGVsIE1vZGVsbyBMaW5lYWwgR2VuZXJhbGl6YWRvIChHTE0pLiBFc3RlIG1vZGVsbywgYSBwZXNhciBkZSBzZXIgc2VuY2lsbG8gZW4gc3UgZXN0cnVjdHVyYSwgbW9zdHLDsyB1biByZW5kaW1pZW50byByZXNwZXRhYmxlLiBTdSBjdXJ2YSBST0MgYWxjYW56w7MgdW4gdmFsb3IgZGUgMC45NTUyLCBsbyBjdWFsIGVzIGluZmVyaW9yIGEgbG9zIGRlIFJhbmRvbSBGb3Jlc3QgeSBTVk0sIHBlcm8gc2lndWUgc2llbmRvIGFkZWN1YWRvIHBhcmEgdGFyZWFzIGRlIGNsYXNpZmljYWNpw7NuLiBMYSBzZW5zaWJpbGlkYWQgZGUgMC45NDM4IGluZGljYSBxdWUgZWwgbW9kZWxvIHRpZW5lIHVuYSBidWVuYSBjYXBhY2lkYWQgcGFyYSBpZGVudGlmaWNhciBsYXMgb2JzZXJ2YWNpb25lcyBwb3NpdGl2YXMsIG1pZW50cmFzIHF1ZSBsYSBlc3BlY2lmaWNpZGFkIGRlIDAuOTQ4NCBlcyBsaWdlcmFtZW50ZSBtZWpvciBxdWUgbGEgc2Vuc2liaWxpZGFkLCBsbyBxdWUgc3VnaWVyZSBxdWUgZWwgbW9kZWxvIHRpZW5lIHVuIGRlc2VtcGXDsW8gbGlnZXJhbWVudGUgbWVqb3IgcGFyYSBkZXRlY3RhciBsYXMgb2JzZXJ2YWNpb25lcyBuZWdhdGl2YXMgZW4gY29tcGFyYWNpw7NuIGNvbiBsYXMgcG9zaXRpdmFzLiBBdW5xdWUgZWwgbW9kZWxvIEdMTSBlcyBmdW5jaW9uYWwsIHN1IHJlbmRpbWllbnRvIGVuIHTDqXJtaW5vcyBkZSBsYSBjdXJ2YSBST0MgZXMgYWxnbyBpbmZlcmlvciBlbiBjb21wYXJhY2nDs24gY29uIGxvcyBtb2RlbG9zIG3DoXMgY29tcGxlam9zIGNvbW8gU1ZNIHkgUmFuZG9tIEZvcmVzdC4gIA0KDQogIDxmb250IHNpemU9IjIiPjIuMyBDb21wYXJhY2nDs24gZGUgTW9kZWxvcyA8L2ZvbnQ+IA0KDQpgYGB7cn0NCiMgQ29tcGFyYXIgZWwgcmVuZGltaWVudG8gZGUgbG9zIG1vZGVsb3MNCnJlc2FtcGxlcyA8LSByZXNhbXBsZXMobGlzdChjYXJ0ID0gbW9kZWxfY2FydCwgcmYgPSBtb2RlbF9yZiwgc3ZtID0gbW9kZWxfc3ZtLCBsb2dpdCA9IG1vZGVsX2xvZ2l0KSkNCg0KIyBWZXIgZWwgcmVzdW1lbiBkZSBsYSBjb21wYXJhY2nDs24gZW50cmUgbG9zIG1vZGVsb3MNCnN1bW1hcnkocmVzYW1wbGVzKQ0KDQojIFZpc3VhbGl6YWNpw7NuIGRlIGxhIGNvbXBhcmFjacOzbg0KYndwbG90KHJlc2FtcGxlcykNCg0KYGBgDQoNCiAgPGZvbnQgc2l6ZT0iMiI+Mi40IEVzdHVkaW8gZGUgbGEgSW1wb3J0YW5jaWEgZGUgbGFzIFZhcmlhYmxlcyA8L2ZvbnQ+IA0KDQoNCkVsIGVzdHVkaW8gZGUgbGEgaW1wb3J0YW5jaWEgZGUgdmFyaWFibGVzIG5vcyBwZXJtaXRlIGlkZW50aWZpY2FyIGN1w6FsZXMgc29uIGxhcyBjYXJhY3RlcsOtc3RpY2FzIHF1ZSBtw6FzIGluZmx1eWVuIGVuIGxhcyBwcmVkaWNjaW9uZXMgcmVhbGl6YWRhcy4gRXN0byBlcyBlc3BlY2lhbG1lbnRlIMO6dGlsIGN1YW5kbyB0cmFiamFtb3MgY29uIGRhdG9zIGRlIMOhbWJpdG8gc2FuaXRhcmlvLCB5YSBxdWUgbm9zIHBlcm1pdGUgcHJpb3JpemFyIGxhcyB2YXJpYWJsZXMgbcOhcyByZWxldmFudGVzIHBhcmEgcmVhbGl6YXIgZGlhZ27Ds3N0aWNvcyBvIHRvbWFyIGRlY2lzaW9uZXMgaW5mb3JtYWRhcy4NCg0KRWwgb2JqZXRpdm8gZGVsIGVzdHVkaW8gZGUgaW1wb3J0YW5jaWEgZGUgdmFyaWFibGVzIGVzIGVsaW1pbmFyIGFxdWVsbGFzIHF1ZSBjb25zaXN0ZW50ZW1lbnRlIHRpZW5lbiBiYWphIGltcG9ydGFuY2lhIGVuIHRvZG9zIGxvcyBtb2RlbG9zOg0KDQoxLSBPYnRlbmVyIGxhIGltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcyBkZSBjYWRhIG1vZGVsby4NCg0KICA8Zm9udCBzaXplPSIyIj4yLjQuMSBFc3R1ZGlvIGRlIGxhIEltcG9ydGFuY2lhIGRlIGxhcyBWYXJpYWJsZXMgY29uIMOBcmJvbCBkZSBEZWNpc2nDs24gKENBUlQpIDwvZm9udD4gDQoNCg0KRW4gbG9zIMOhcmJvbGVzIGRlIGRlY2lzacOzbiwgbGEgaW1wb3J0YW5jaWEgZGUgdmFpcmFibGVzIHNlIGNhbGN1bGEgZW4gZnVuY2nDs24gZGUgbGFzIGdhbmFuY2lhcyBkZSByZWR1Y2Npw7NuIGRlIGltcHVyZXphLCBwb3IgZWplbXBsbywgbGEgcmVkdWNjacOzbiBkZSBsYSBlbnRyb3DDrWEgbyBkZWwgw61uZGljZSBHaW5pIGVuIGxvcyBub2RvcyBkb25kZSBsYSB2YXJpYWJsZXMgZXMgdXRpbGl6YWRhIHBhcmEgZGl2aWRyIGxvcyBkYXRvcy4NCg0KYGBge3J9DQpwbG90KGltcG9ydGFuY2VfY2FydCwgbWFpbiA9ICJJbXBvcnRhbmNpYSBkZSBWYXJpYWJsZXMgLSBDQVJUIikNCmBgYA0KDQogPGZvbnQgc2l6ZT0iMiI+Mi40LjIgRXN0dWRpbyBkZSBsYSBJbXBvcnRhbmNpYSBkZSBsYXMgVmFyaWFibGVzIGNvbiBSYW5kb20gRm9yZXN0IDwvZm9udD4gDQogDQogRW4gUmFuZG9tIEZvcmVzdCBsYSBpbXBvcnRhbmNpYSBzZSBjYWxjdWxhIG1lZGlhbnRlIGRvcyBlbmZvcXVlcyBjb211bmVzOg0KIA0KICAtIEltcG9ydGFuY2lhIGJhc2FkYSBlbiBwZXJtdXRhY2nDs246IEV2YWzDumEgY8OzbW8gY2FtYmlhIGxhIHByZWNpc2nDs24gZGVsIG1vZGVsbyBhbCBwZXJtdXRhciBhbGVhdG9yaWFtZW50ZSBsb3MgdmFsb3JlcyBkZSB1bmEgdmFyaWFibGUuDQogIC0gUmVkdWNjacOzbiBwcm9tZWRpbyBkZSBsYSBpbXB1cmV6YTogQ2FsY3VsYSBjdcOhbnRvIGNvbnRyaWJ1eWUgdW5hIHZhcmlhYmxlIGEgbGEgcmVkdWNjacOzbiBkZSBpbXB1cmV6YSBhIHRyYXbDqXMgZGUgdG9kb3MgbG9zIMOhcmJvbGVzIGRlbCBib3NxdWUuDQogIA0KYGBge3J9DQpwbG90KGltcG9ydGFuY2VfcmYsIG1haW4gPSAiSW1wb3J0YW5jaWEgZGUgVmFyaWFibGVzIC0gUmFuZG9tIEZvcmVzdCIpDQpgYGANCiAgDQogDQogPGZvbnQgc2l6ZT0iMiI+Mi40LjMgRXN0dWRpbyBkZSBsYSBJbXBvcnRhbmNpYSBkZSBsYXMgVmFyaWFibGVzIGNvbiBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIChTVk0pIDwvZm9udD4gIA0KIA0KIEVsIGNhbGN1bG8gZGUgbGEgaW1wb3J0YW5jaWEgZW4gU1ZNIG5vIGVzIHRhbiBkaXJlY3RvLCB5YSBxdWUgZXN0ZSBtb2RlbG8gbm8gc2UgYmFzYSBlbiB1bmEgZXN0cnVjdHVyYSBqZXLDoXJxdWljYSBvIGVuIHVuYSBncmVnYWNpw7NuIGRlIMOhcmJvbGVzLiBQb2RlbW9zIGVzdGltYXIgbGEgaW1wb3J0YW5jaWEgZGUgbGFzIHZhcmlhYmxlcyBtZWRpYW50ZSBhbsOhbGlzaXMgcG9zdC1ob2MsIGNvbW8gbGEgZXZhbHVhY2nDs24gZGUgbG9zIGNvZWZpY2llbnRlcyBlbiBlbCBlc3BhY2lvIGZvcm1hZG8gcG9yIGVsIG7DumNsZW8gcmFkaWFsLg0KIA0KYGBge3J9DQpwbG90KGltcG9ydGFuY2Vfc3ZtLCBtYWluID0gIkltcG9ydGFuY2lhIGRlIFZhcmlhYmxlcy0gU1ZNIikNCmBgYA0KIA0KIDxmb250IHNpemU9IjIiPjIuNC40IEVzdHVkaW8gZGUgbGEgSW1wb3J0YW5jaWEgZGUgbGFzIFZhcmlhYmxlcyBjb24gUmVncmVzacOzbiBMb2fDrXN0aWNhIDwvZm9udD4gDQogDQogRW4gcmVncmVzacOzbiBsb2fDrXN0aWNhLCBsYSBpbXBvcnRhbmNpYSBkZSB2YXJpYWJsZXMgc2UgcHVlZGUgYW5hbGl6YXIgbWVkaWFudGUgbG9zIGNvZWZpY2llbnRlcyBlc3RpbWFkb3MgZGVsIG1vZGVsby4gRXN0b3MgY29lZmljaWVudGVzIGluZGljYW4gbGEgbWFnbml0dWQgeSBsYSBkaXJlY2Npw7NuIGRlbCBlZmVjdG8gZGUgY2FkYSB2YXJpYWJsZSBlbiBsYSBwcm9iYWJpbGlkYWQgZGUgcXVlIHVuIHR1bW9yIHNlYSBtYWxpZ25vLg0KIA0KYGBge3J9DQpwbG90KGltcG9ydGFuY2VfbG9naXQsIG1haW4gPSAiSW1wb3J0YW5jaWEgZGUgVmFyaWFibGVzIC0gR0xNIikNCmBgYA0KIA0KIDxmb250IHNpemU9IjIiPjIuNC40IENvbXBhcmFjacOzbiBHZW5lcmFsIGRlbCBlc3R1ZGlvIGRlIGxhIEltcG9ydGFuY2lhIGRlIGxhcyBWYXJpYWJsZXMgPC9mb250PiANCiANCiANCg0KMi0gTm9ybWFsaXphciBsYSBpbXBvcnRhbmNpYSBwYXJhIGNvbXBhcmFybGEgZW50cmUgbW9kZWxvcy4NCjMtIENhbGN1bGFyIHVuYSBtw6l0cmljYSBjb25zb2xpZGFkYSBkZSBpbXBvcnRhbmNpYSBwcm9tZWRpby4NCjQtIElkZW50aWZpY2FyIGxhcyB2YXJpYWJsZXMgY29uIG1lbm9yIGltcGFjdG8gZW4gdG9kb3MgbG9zIG1vZGVsb3MuDQoNCmBgYHtyfQ0KIyBDb25zb2xpZGFyIGltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcw0KaW1wb3J0YW5jZV9jb21iaW5lZCA8LSBtZXJnZSgNCiAgbWVyZ2UoaW1wb3J0YW5jZV9jYXJ0X2RmLCBpbXBvcnRhbmNlX3JmX2RmLCBieSA9ICJ2YXJpYWJsZSIsIHN1ZmZpeGVzID0gYygiX2NhcnQiLCAiX3JmIikpLA0KICBtZXJnZShpbXBvcnRhbmNlX3N2bV9kZiwgaW1wb3J0YW5jZV9sb2dpdF9kZiwgYnkgPSAidmFyaWFibGUiLCBzdWZmaXhlcyA9IGMoIl9zdm0iLCAiX2xvZ2l0IikpLA0KICBieSA9ICJ2YXJpYWJsZSINCikNCg0KIyBQcm9tZWRpbyBkZSBpbXBvcnRhbmNpYQ0KaW1wb3J0YW5jZV9jb21iaW5lZCRtZWFuX2ltcG9ydGFuY2UgPC0gcm93TWVhbnMoaW1wb3J0YW5jZV9jb21iaW5lZFssIC0xXSwgbmEucm0gPSBUUlVFKQ0KDQojIFNlbGVjY2lvbmFyIGxhcyBtZW5vcyBpbXBvcnRhbnRlcyAocG9yIGRlYmFqbyBkZSB1biB1bWJyYWwsIHBvciBlamVtcGxvLCBlbCBwZXJjZW50aWwgMjApDQp0aHJlc2hvbGQgPC0gcXVhbnRpbGUoaW1wb3J0YW5jZV9jb21iaW5lZCRtZWFuX2ltcG9ydGFuY2UsIDAuMikNCmxlYXN0X2ltcG9ydGFudF92YXJzIDwtIGltcG9ydGFuY2VfY29tYmluZWQkdmFyaWFibGVbaW1wb3J0YW5jZV9jb21iaW5lZCRtZWFuX2ltcG9ydGFuY2UgPD0gdGhyZXNob2xkXQ0KDQojIEVsaW1pbmFyIGVzdGFzIHZhcmlhYmxlcyBkZWwgZGF0YXNldCBvcmlnaW5hbA0KZGF0YV9yZWR1Y2VkIDwtIGRhdGFbLCAhKG5hbWVzKGRhdGEpICVpbiUgbGVhc3RfaW1wb3J0YW50X3ZhcnMpXQ0KDQojIEd1YXJkYXIgZWwgZGF0YXNldCByZWR1Y2lkbw0Kd3JpdGUuY3N2KGRhdGFfcmVkdWNlZCwgImRhdGFfcmVkdWNlZC5jc3YiLCByb3cubmFtZXMgPSBGQUxTRSkNCg0KYGBgDQogDQpgYGB7cn0NCmRhdGFfcmVkdWNlZA0KZGltKGRhdGFfcmVkdWNlZCkNCg0KYGBgDQoNCmBgYHtyfQ0KY29sbmFtZXMoZGF0YV9yZWR1Y2VkKQ0KYGBgDQoNCmBgYHtyfQ0KY29sbmFtZXMoZGF0YSkNCmBgYA0KRWwgc2lndWllbnRlIHBhc28gc2Vyw6EgcmVwZXRpciBudWVzdHJvIGFuw6FsaXNpcyBzdXBlcnZpc2FkbyBlc3RhIHZleiB1c2FuZG8gZWwgZGF0YXNldCByZWR1Y2lkbyBlbiBlbCBxdWUgbm8gYXBhcmVjZW4gbGFzIGNvbHVtbmFzICJtZW5vcyBpbXBvcnRhbnRlcyIuIERlIGVzdGEgZm9ybWEgcG9kcmVtb3MgYW5hbGl6YXIgc2kgbG9zIHJlc3VsdGFkb3MgbWVqb3JhciwgZW1wZW9yYW4gbyBubyBhZmVjdGFuIGNvbiBlbCBlc3R1ZGlvIGRlIGltcG9ydGFuY2lhIGRlIHZhcmlhbmxlczoNCg0KDQogPGZvbnQgc2l6ZT0iMiI+My4gQW7DoWxpc2lzIFN1cGVydmlzYWRvIGNvbiBkYXRhc2V0IHJlZHVjaWRvLiA8L2ZvbnQ+IA0KICA8Zm9udCBzaXplPSIyIj4zLjEgw4FyYm9sIGRlIERlY2lzacOzbiAoQ0FSVCkgPC9mb250PiANCiAgDQpgYGB7cn0NCiMgw4FyYm9sIGRlIERlY2lzacOzbiB1dGlsaXphbmRvIEstZm9sZCBjcm9zcy12YWxpZGF0aW9uDQptb2RlbF9jYXJ0IDwtIHRyYWluKA0KICBkaWFnbm9zaXMgfiAuLCAgICAgICAgIyBVc2Ftb3MgdG9kYXMgbGFzIHZhcmlhYmxlcyBwcmVkaWN0b3Jhcw0KICBkYXRhID0gZGF0YV9yZWR1Y2VkLA0KICBtZXRob2QgPSAicnBhcnQiLCAgICAgIyDDgXJib2wgZGUgZGVjaXNpw7NuIChDQVJUKQ0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX2NhcnQpDQpgYGANCiAgDQogPGZvbnQgc2l6ZT0iMiI+My4yIFJhbmRvbSBGb3Jlc3QgPC9mb250PiANCiANCmBgYHtyfQ0KbW9kZWxfcmYgPC0gdHJhaW4oDQogIGRpYWdub3NpcyB+IC4sICAgICAgICAjIFVzYW1vcyB0b2RhcyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzDQogIGRhdGEgPSBkYXRhX3JlZHVjZWQsDQogIG1ldGhvZCA9ICJyZiIsICAgICAgICAjIFJhbmRvbSBGb3Jlc3QNCiAgdHJDb250cm9sID0gdHJhaW5fY29udHJvbCwgICMgQ29udHJvbCBkZSB2YWxpZGFjacOzbiBjcnV6YWRhDQogIG1ldHJpYyA9ICJST0MiICAgICAgICAjIEV2YWx1YXIgdXRpbGl6YW5kbyBBVUMgKMOBcmVhIGJham8gbGEgY3VydmEgUk9DKQ0KKQ0KDQojIFZlciBlbCByZXN1bWVuIGRlbCBtb2RlbG8gZW50cmVuYWRvDQpwcmludChtb2RlbF9yZikNCg0KYGBgDQogDQogPGZvbnQgc2l6ZT0iMiI+My4zIFN1cHBvcnQgVmVjdG9yIE1hY2hpbmUgKFNWTSkgPC9mb250PiANCiANCmBgYHtyfQ0KI2luc3RhbGwucGFja2FnZXMoImtlcm5sYWIiKQ0KbGlicmFyeShrZXJubGFiKQ0KDQptb2RlbF9zdm0gPC0gdHJhaW4oDQogIGRpYWdub3NpcyB+IC4sICAgICAgICAjIFVzYW1vcyB0b2RhcyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzDQogIGRhdGEgPSBkYXRhX3JlZHVjZWQsDQogIG1ldGhvZCA9ICJzdm1SYWRpYWwiLCAgIyBTdXBwb3J0IFZlY3RvciBNYWNoaW5lIGNvbiBrZXJuZWwgcmFkaWFsDQogIHRyQ29udHJvbCA9IHRyYWluX2NvbnRyb2wsICAjIENvbnRyb2wgZGUgdmFsaWRhY2nDs24gY3J1emFkYQ0KICBtZXRyaWMgPSAiUk9DIiAgICAgICAgIyBFdmFsdWFyIHV0aWxpemFuZG8gQVVDICjDgXJlYSBiYWpvIGxhIGN1cnZhIFJPQykNCikNCg0KIyBWZXIgZWwgcmVzdW1lbiBkZWwgbW9kZWxvIGVudHJlbmFkbw0KcHJpbnQobW9kZWxfc3ZtKQ0KDQpgYGANCiANCiA8Zm9udCBzaXplPSIyIj4zLjQgUmVncmVzacOzbiBMb2fDrXN0aWNhIDwvZm9udD4gDQoNCmBgYHtyfQ0KbW9kZWxfbG9naXQgPC0gdHJhaW4oDQogIGRpYWdub3NpcyB+IC4sICAgICAgICAjIFVzYW1vcyB0b2RhcyBsYXMgdmFyaWFibGVzIHByZWRpY3RvcmFzDQogIGRhdGEgPSBkYXRhX3JlZHVjZWQsDQogIG1ldGhvZCA9ICJnbG0iLCAgICAgICAjIFJlZ3Jlc2nDs24gTG9nw61zdGljYQ0KICB0ckNvbnRyb2wgPSB0cmFpbl9jb250cm9sLCAgIyBDb250cm9sIGRlIHZhbGlkYWNpw7NuIGNydXphZGENCiAgbWV0cmljID0gIlJPQyIgICAgICAgICMgRXZhbHVhciB1dGlsaXphbmRvIEFVQyAow4FyZWEgYmFqbyBsYSBjdXJ2YSBST0MpDQopDQoNCiMgVmVyIGVsIHJlc3VtZW4gZGVsIG1vZGVsbyBlbnRyZW5hZG8NCnByaW50KG1vZGVsX2xvZ2l0KQ0KYGBgDQogDQogDQogPGZvbnQgc2l6ZT0iMiI+My41IENvbXBhcmFjacOzbiBkZSByZXN1bHRhZG9zIGFuw6FsaXNpcyBzdXBlcnZpc2FkbyBkYXRhc2V0IGluY2lhbCB2cyBkYXRhc2V0IHJlZHVjaWRvOiA8L2ZvbnQ+IA0KDQpgYGB7cn0NCg0KDQojIENyZWFyIGxvcyBkYXRvcyBwYXJhIGNhZGEgbW9kZWxvIGVuIGZvcm1hdG8gZGUgZGF0YSBmcmFtZQ0KIyBEYXRhc2V0IGluaWNpYWwNCmluaWNpYWwgPC0gZGF0YS5mcmFtZSgNCiAgTW9kZWxvID0gYygiQ0FSVCIsICJSYW5kb20gRm9yZXN0IiwgIlNWTSIsICJHTE0iKSwNCiAgUk9DID0gYygwLjk0MzEsIDAuOTkyNywgMC45OTU0LCAwLjk1MjgpLA0KICBTZW5zID0gYygwLjk0MTEsIDAuOTgwNCwgMC45NzQ5LCAwLjk0NjgpLA0KICBTcGVjID0gYygwLjg5MTEsIDAuOTE5OCwgMC45NjI1LCAwLjk0MzQpLA0KICBEYXRhc2V0ID0gIkluaWNpYWwiDQopDQoNCiMgRGF0YXNldCByZWR1Y2lkbw0KcmVkdWNpZG8gPC0gZGF0YS5mcmFtZSgNCiAgTW9kZWxvID0gYygiQ0FSVCIsICJSYW5kb20gRm9yZXN0IiwgIlNWTSIsICJHTE0iKSwNCiAgUk9DID0gYygwLjk0NTEsIDAuOTkwOSwgMC45OTQ2LCAwLjk3NTkpLA0KICBTZW5zID0gYygwLjk1MjMsIDAuOTY5MSwgMC45NjkyLCAwLjk2NjMpLA0KICBTcGVjID0gYygwLjg5MTUsIDAuOTI5NiwgMC45NjIxLCAwLjk1NzkpLA0KICBEYXRhc2V0ID0gIlJlZHVjaWRvIg0KKQ0KDQojIFVuaXIgYW1ib3MgZGF0YXNldHMgZW4gdW5hIHNvbGEgdGFibGEgcG9yIGVsIG1vZGVsbw0KY29tcGFyYXRpdmEgPC0gbWVyZ2UoaW5pY2lhbCwgcmVkdWNpZG8sIGJ5ID0gIk1vZGVsbyIpDQoNCiMgSW1wcmltaXIgbGEgdGFibGEgZW4gY29uc29sYQ0KcHJpbnQoY29tcGFyYXRpdmEpDQoNCmBgYA0KDQpgYGB7cn0NCiMgSW5zdGFsYXIgZmxleHRhYmxlIHNpIG5vIGVzdMOhIGluc3RhbGFkbw0KI2luc3RhbGwucGFja2FnZXMoImZsZXh0YWJsZSIpDQoNCiMgQ2FyZ2FyIGxhIGxpYnJlcsOtYQ0KbGlicmFyeShmbGV4dGFibGUpDQoNCiMgQ3JlYXIgbGEgdGFibGEgY29uIGZsZXh0YWJsZQ0KdGFibGFfZmxleCA8LSBmbGV4dGFibGUoY29tcGFyYXRpdmEpDQoNCiMgSW1wcmltaXIgbGEgdGFibGEgZW4gdW4gZW50b3JubyBxdWUgc29wb3J0ZSBmbGV4dGFibGUgKHBvciBlamVtcGxvLCBSU3R1ZGlvKQ0KdGFibGFfZmxleA0KDQpgYGANCg0KLSBDQVJUOg0KDQpMYSBzZW5zaWJpbGlkYWQgbWVqb3JhIGNvbiBlbCBkYXRhc2V0IHJlZHVjaWRvLCBsbyBxdWUgaW5kaWNhIHVuYSBtZWpvciBjYXBhY2lkYWQgcGFyYSBkZXRlY3RhciBjYXNvcyBwb3NpdGl2b3MuIEVzdG8gcHVlZGUgZGViZXJzZSBhIHF1ZSBsYXMgdmFyaWFibGVzIG1lbm9zIHJlbGV2YW50ZXMgZ2VuZXJhYmFuIGRpdmlzaW9uZXMgaW5uZWNlc2FyaWFzIGVuIGVsIMOhcmJvbC4NCg0KLSBSYW5kb20gRm9yZXN0Og0KDQpBdW5xdWUgaGF5IHVuYSBsZXZlIGRpc21pbnVjacOzbiBlbiBST0MgeSBzZW5zaWJpbGlkYWQsIGxhIGVzcGVjaWZpY2lkYWQgbWVqb3JhLiBFc3RvIGltcGxpY2EgcXVlIGVsIG1vZGVsbyBjb24gZWwgZGF0YXNldCByZWR1Y2lkbyBpZGVudGlmaWNhIG1lam9yIGxvcyBjYXNvcyBuZWdhdGl2b3MuDQoNCi0gU1ZNOg0KDQpFbCBpbXBhY3RvIGRlbCBkYXRhc2V0IHJlZHVjaWRvIGVzIG3DrW5pbW8sIHBlcm8gbXVlc3RyYSB1bmEgbGlnZXJhIGRpc21pbnVjacOzbiBlbiB0b2RhcyBsYXMgbcOpdHJpY2FzLiBFc3RvIHN1Z2llcmUgcXVlIGxhcyB2YXJpYWJsZXMgZWxpbWluYWRhcyB0ZW7DrWFuIHVuYSBpbmZsdWVuY2lhIG1lbm9yLCBwZXJvIG5vIGVyYW4gY29tcGxldGFtZW50ZSBpcnJlbGV2YW50ZXMuDQoNCi0gR0xNOg0KDQpFbCBtb2RlbG8gY29uIGRhdGFzZXQgcmVkdWNpZG8gbXVlc3RyYSBtZWpvcmFzIGNsYXJhcyBlbiB0b2RhcyBsYXMgbcOpdHJpY2FzLCBsbyBxdWUgcmVmdWVyemEgbGEgaWRlYSBkZSBxdWUgbGFzIHZhcmlhYmxlcyBlbGltaW5hZGFzIG5vIGVyYW4gw7p0aWxlcyBwYXJhIGVzdGUgZW5mb3F1ZSBsaW5lYWwuDQoNCkVuIHJlc3VtZW46IExhIGVsaW1pbmFjacOzbiBkZSB2YXJpYWJsZXMgbWVub3MgaW1wb3J0YW50ZXMgbWFudGllbmUgdW4gcmVuZGltaWVudG8gY29tcGFyYWJsZSwgZSBpbmNsdXNvIG1lam9yYSBhbGd1bmFzIG3DqXRyaWNhcyBlc3BlY8OtZmljYXMgZW4gY2llcnRvcyBtb2RlbG9zLg0KDQpFc3RvIHN1Z2llcmUgcXVlIGxhcyB2YXJpYWJsZXMgZWxpbWluYWRhcyBubyBhcG9ydGFiYW4gaW5mb3JtYWNpw7NuIHJlbGV2YW50ZSBvIGluY2x1c28gcG9kw61hbiBpbnRyb2R1Y2lyIHJ1aWRvLg0KDQpSZWR1Y2lyIGVsIG7Dum1lcm8gZGUgdmFyaWFibGVzIHRpZW5lIHZlbnRhamFzIHByw6FjdGljYXMsIGNvbW8gbWVub3JlcyByZXF1ZXJpbWllbnRvcyBjb21wdXRhY2lvbmFsZXMgeSBtYXlvciBpbnRlcnByZXRhYmlsaWRhZCwgc2luIGNvbXByb21ldGVyIGVsIHJlbmRpbWllbnRvLg0KDQo8Zm9udCBzaXplPSI0Ij4gNC4gQW7DoWxpc2lzIE5vIFN1cGVydmlzYWRvLjwvZm9udD4gDQoNCkVsIGFuw6FsaXNpcyBubyBzdXBlcnZpc2FkbyBzZSBjZW50cmEgZW4gZXhwbG9yYXIgcGF0cm9uZXMgbyBlc3RydWN0dXJhcyBlbiBsb3MgZGF0b3Mgc2luIHV0bGl6YXIgZXRpcXVldGFzIG8gY2xhc2VzLiBQYXJhIMOpc3RlIGFuw6FsaXNpcywgc2UgdXR1bGl6YXLDoW4gdMOpY25pY2FzIGNvbW8gY2x1c3RlcmluZyBvIHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkLg0KDQoNCjxmb250IHNpemU9IjIiPjQuMSBQcmVwYXJhY2nDs24gZGVsIERhdGFzZXQgPC9mb250PiANCg0KRWwgcHJpbWVyIHBhc28gc2Vyw6EgZWxpbWluYXIgbGEgY29sdW1uYSAiZGlhZ25vc2lzIiB5IG5vcm1hbGl6YXIgZWwgZGF0YXNldCwgeWEgcXVlIG11Y2hhcyB0w6ljbmljYXMgZGUgY2x1c3RlcmluZyB5IHJlZHVjY2nDs24gZGUgZGltZW5zaW9uYWxpZGFkIHNvbiBzZW5zaWJsZXMgYSBsYXMgZXNjYWxhcyBkZSBsYXMgdmFyaWFibGVzLg0KDQpgYGB7cn0NCmRhdGFfVW5TdXBlcnZpc2VkIDwtIGRhdGEgJT4lIHNlbGVjdCgtZGlhZ25vc2lzKQ0KaGVhZChkYXRhX1VuU3VwZXJ2aXNlZCkNCmRhdGFfVW5TdXBlcnZpc2VkJGRpYWdub3Npcw0KYGBgDQoNCmBgYHtyfQ0KIyBOb3JtYWxpemFyIGxvcyBkYXRvcyAoc2luIGxhIHZhcmlhYmxlIGRlIHJlc3B1ZXN0YSBzaSBleGlzdGUpDQpwcmVwcm9jZXNzX3BhcmFtcyA8LSBwcmVQcm9jZXNzKGRhdGFfVW5TdXBlcnZpc2VkWywgLW5jb2woZGF0YV9VblN1cGVydmlzZWQpXSwgbWV0aG9kID0gYygiY2VudGVyIiwgInNjYWxlIikpDQpkYXRhX25vcm1hbGl6ZWQgPC0gcHJlZGljdChwcmVwcm9jZXNzX3BhcmFtcywgZGF0YV9VblN1cGVydmlzZWRbLCAtbmNvbChkYXRhX1VuU3VwZXJ2aXNlZCldKQ0KDQpgYGANCg0KPGZvbnQgc2l6ZT0iMiI+NC4yIENsdXN0ZXJpbmc6IERldGVybWluYWNpw7NuIGRlbCBOw7ptZXJvIMOTcHRpbW8gZGUgQ2x1c3RlcnMgPC9mb250Pg0KDQpQYXJhIGVsbG8gdXRpbGl6YXJlbW9zIGVsIG3DqXRvZG8gZGVsIGNvZG8geSBlbCDDrW5kaWNlIHNpbGhvdWV0dGU6DQoNCmBgYHtyfQ0KIyBNw6l0b2RvIGRlbCBjb2RvDQp3c3MgPC0gKG5yb3coZGF0YV9ub3JtYWxpemVkKSAtIDEpICogc3VtKGFwcGx5KGRhdGFfbm9ybWFsaXplZCwgMiwgdmFyKSkNCmZvciAoaSBpbiAyOjE1KSB3c3NbaV0gPC0gc3VtKGttZWFucyhkYXRhX25vcm1hbGl6ZWQsIGNlbnRlcnMgPSBpKSR3aXRoaW5zcykNCg0KcGxvdCgxOjE1LCB3c3MsIHR5cGUgPSAiYiIsIHhsYWIgPSAiTsO6bWVybyBkZSBDbHVzdGVycyIsIHlsYWIgPSAiU3VtYSBkZSBDdWFkcmFkb3MgSW50ZXJub3MiKQ0KDQojIMONbmRpY2Ugc2lsaG91ZXR0ZQ0KbGlicmFyeShjbHVzdGVyKQ0Kc2lsaG91ZXR0ZV9zY29yZXMgPC0gbnVtZXJpYygpDQpmb3IgKGkgaW4gMjoxNSkgew0KICBrbSA8LSBrbWVhbnMoZGF0YV9ub3JtYWxpemVkLCBjZW50ZXJzID0gaSkNCiAgc2lsaG91ZXR0ZV9zY29yZXNbaV0gPC0gbWVhbihzaWxob3VldHRlKGttJGNsdXN0ZXIsIGRpc3QoZGF0YV9ub3JtYWxpemVkKSlbLCAzXSkNCn0NCnBsb3QoMjoxNSwgc2lsaG91ZXR0ZV9zY29yZXNbLTFdLCB0eXBlID0gImIiLCB4bGFiID0gIk7Dum1lcm8gZGUgQ2x1c3RlcnMiLCB5bGFiID0gIlB1bnRhamUgZGUgU2lsaG91ZXR0ZSIpDQoNCmBgYA0KDQoNCjxmb250IHNpemU9IjIiPjQuMyBLLU1lYW5zIDwvZm9udD4NCg0KVW5hIHZleiBkZXRlcm1pbmFkbyBlbCBuw7ptZXJvIMOzcHRpbW8gZGUgY2x1c3RlcnMsIHNlIGVqZWN1dGEgSy1tZWFuczoNCg0KYGBge3J9DQpzZXQuc2VlZCgxMjMpDQoNCm9wdGltYWxfY2x1c3RlcnMgPSA0DQoNCmttZWFuc19tb2RlbCA8LSBrbWVhbnMoZGF0YV9ub3JtYWxpemVkLCBjZW50ZXJzID0gb3B0aW1hbF9jbHVzdGVycywgbnN0YXJ0ID0gMjUpDQoNCiMgQWdyZWdhciBldGlxdWV0YXMgZGUgY2x1c3RlciBhbCBkYXRhc2V0IG9yaWdpbmFsDQpkYXRhJGNsdXN0ZXIgPC0ga21lYW5zX21vZGVsJGNsdXN0ZXINCg0KYGBgDQoNCg0KPGZvbnQgc2l6ZT0iMiI+NC40IFBDQSA8L2ZvbnQ+DQoNCkVsIHByw7N4aW1vIHBhc28gZGUgbnVlc3RybyBhbsOhbGlzaXMgbm8gc3VwZXJ2aXNhZG8gc2Vyw6EgdXRpbGl6YXIgZWwgQW7DoWxpc2lzIGRlIENvbXBvbmVudGVzIFByaW5jaXBhbGVzIHBhcmEgdmlzdWFsaXphciBsYSBlc3RydWN0dXJhIGRlIGxvcyBkYXRvcyB5IHZhbGlkYXIgbG9zIHJlc3VsdGFkb3MgZGVsIGNsdXN0ZXJpbmcuDQoNCg0KYGBge3J9DQojIFBDQQ0KcGNhX21vZGVsIDwtIHByY29tcChkYXRhX25vcm1hbGl6ZWQsIHNjYWxlID0gVFJVRSkNCg0KIyBWaXN1YWxpemFjacOzbiBkZSBsb3MgZG9zIHByaW1lcm9zIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzDQpsaWJyYXJ5KGdncGxvdDIpDQpwY2FfZGF0YSA8LSBkYXRhLmZyYW1lKHBjYV9tb2RlbCR4WywgMToyXSwgY2x1c3RlciA9IGFzLmZhY3RvcihkYXRhJGNsdXN0ZXIpKQ0KZ2dwbG90KHBjYV9kYXRhLCBhZXMoeCA9IFBDMSwgeSA9IFBDMiwgY29sb3IgPSBjbHVzdGVyKSkgKw0KICBnZW9tX3BvaW50KCkgKw0KICB0aGVtZV9taW5pbWFsKCkgKw0KICBsYWJzKHRpdGxlID0gIkNsdXN0ZXJpbmcgdmlzdWFsaXphZG8gZW4gZXNwYWNpbyBQQ0EiKQ0KDQpgYGANCg0KU2UgdmlzdWFsaXphbiBjbGFyYW1lbnRlIGN1YXRybyBjbMO6c3RlcmVzLg0KDQpMb3MgY2zDunN0ZXJlcyBlc3TDoW4gYmllbiBkZWZpbmlkb3MgeSBzZXBhcmFkb3MgZXNwYWNpYWxtZW50ZSwgZXNwZWNpYWxtZW50ZSBlbnRyZSBlbCBjbMO6c3RlciBww7pycHVyYSB5IGxvcyBkZW3DoXMuDQoNCkhheSB1biBncmFkbyBtZW5vciBkZSBzb2xhcGFtaWVudG8gZW50cmUgbG9zIGNsw7pzdGVyZXMsIGxvIHF1ZSBzdWdpZXJlIHF1ZSBsYXMgdmFyaWFibGVzIG9yaWdpbmFsZXMgb2ZyZWNlbiB1bmEgc2VwYXJhY2nDs24gbcOhcyBjbGFyYSBlbnRyZSBsYXMgY2xhc2VzIGVuIGVzdGUgZXNwYWNpby4NClJlc3BlY3RvIGFsIGVzcGFjaW8gZGUgVmFyaWFjacOzbiwgbG9zIHB1bnRvcyBlc3TDoW4gZGlzdHJpYnVpZG9zIGVuIHRvcm5vIGEgY29vcmRlbmFkYXMgbcOhcyBjZW50cmFkYXMgKGRlIC0xNSBhIDUgZW4gUEMxIHkgLTEwIGEgMTAgZW4gUEMyKS4NCg0KVmFtb3MgdGFtYmnDqW4gYSByZXBldGlyIGVsIGNsdXN0ZXJpbmcgY29uIGVsIGRhdGFzZXQgcmVkdWNpZG8gZ2VuZXJhZG8gZHVyYW50ZSBlbCBlc3R1ZGlvIGRlIGxhIGltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXM6DQoNCjxmb250IHNpemU9IjIiPjQuNSBBbsOhbGlzaXMgTm8gU3VwZXJ2aXNhZG8gY29uIERhdGFzZXQgUmVkdWNpZG8gc2Vnw7puIGltcG9ydGFuY2lhIGRlIHZhcmlhYmxlcyA8L2ZvbnQ+DQoNCg0KDQpgYGB7cn0NCmRhdGFfVW5TdXBlcnZpc2VkX3JlZHVjZWQgPC0gZGF0YV9yZWR1Y2VkICU+JSBzZWxlY3QoLWRpYWdub3NpcykNCmhlYWQoZGF0YV9VblN1cGVydmlzZWRfcmVkdWNlZCkNCmRhdGFfVW5TdXBlcnZpc2VkX3JlZHVjZWQkZGlhZ25vc2lzDQpgYGANCg0KYGBge3J9DQojIE5vcm1hbGl6YXIgbG9zIGRhdG9zIChzaW4gbGEgdmFyaWFibGUgZGUgcmVzcHVlc3RhIHNpIGV4aXN0ZSkNCnByZXByb2Nlc3NfcGFyYW1zIDwtIHByZVByb2Nlc3MoZGF0YV9VblN1cGVydmlzZWRfcmVkdWNlZFssIC1uY29sKGRhdGFfVW5TdXBlcnZpc2VkX3JlZHVjZWQpXSwgbWV0aG9kID0gYygiY2VudGVyIiwgInNjYWxlIikpDQpkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCA8LSBwcmVkaWN0KHByZXByb2Nlc3NfcGFyYW1zLCBkYXRhX1VuU3VwZXJ2aXNlZF9yZWR1Y2VkWywgLW5jb2woZGF0YV9VblN1cGVydmlzZWRfcmVkdWNlZCldKQ0KYGBgDQoNCg0KYGBge3J9DQojIE3DqXRvZG8gZGVsIGNvZG8NCndzcyA8LSAobnJvdyhkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCkgLSAxKSAqIHN1bShhcHBseShkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCwgMiwgdmFyKSkNCmZvciAoaSBpbiAyOjE1KSB3c3NbaV0gPC0gc3VtKGttZWFucyhkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCwgY2VudGVycyA9IGkpJHdpdGhpbnNzKQ0KDQpwbG90KDE6MTUsIHdzcywgdHlwZSA9ICJiIiwgeGxhYiA9ICJOw7ptZXJvIGRlIENsdXN0ZXJzIiwgeWxhYiA9ICJTdW1hIGRlIEN1YWRyYWRvcyBJbnRlcm5vcyIpDQoNCiMgw41uZGljZSBzaWxob3VldHRlDQpsaWJyYXJ5KGNsdXN0ZXIpDQpzaWxob3VldHRlX3Njb3JlcyA8LSBudW1lcmljKCkNCmZvciAoaSBpbiAyOjE1KSB7DQogIGttIDwtIGttZWFucyhkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCwgY2VudGVycyA9IGkpDQogIHNpbGhvdWV0dGVfc2NvcmVzW2ldIDwtIG1lYW4oc2lsaG91ZXR0ZShrbSRjbHVzdGVyLCBkaXN0KGRhdGFfbm9ybWFsaXplZF9yZWR1Y2VkKSlbLCAzXSkNCn0NCnBsb3QoMjoxNSwgc2lsaG91ZXR0ZV9zY29yZXNbLTFdLCB0eXBlID0gImIiLCB4bGFiID0gIk7Dum1lcm8gZGUgQ2x1c3RlcnMiLCB5bGFiID0gIlB1bnRhamUgZGUgU2lsaG91ZXR0ZSIpDQpgYGANCg0Kay1tZWFucw0KDQpgYGB7cn0NCnNldC5zZWVkKDEyMykNCg0Kb3B0aW1hbF9jbHVzdGVyc18yID0gNQ0KDQprbWVhbnNfbW9kZWxfcmVkdWNlZCA8LSBrbWVhbnMoZGF0YV9ub3JtYWxpemVkX3JlZHVjZWQsIGNlbnRlcnMgPSBvcHRpbWFsX2NsdXN0ZXJzXzIsIG5zdGFydCA9IDI1KQ0KDQojIEFncmVnYXIgZXRpcXVldGFzIGRlIGNsdXN0ZXIgYWwgZGF0YXNldCBvcmlnaW5hbA0KZGF0YV9yZWR1Y2VkJGNsdXN0ZXIgPC0ga21lYW5zX21vZGVsX3JlZHVjZWQkY2x1c3Rlcg0KYGBgDQoNCmBgYHtyfQ0KIyBQQ0ENCnBjYV9tb2RlbF9yZWR1Y2VkIDwtIHByY29tcChkYXRhX25vcm1hbGl6ZWRfcmVkdWNlZCwgc2NhbGUgPSBUUlVFKQ0KDQojIFZpc3VhbGl6YWNpw7NuIGRlIGxvcyBkb3MgcHJpbWVyb3MgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMNCmxpYnJhcnkoZ2dwbG90MikNCnBjYV9kYXRhX3JlZHVjZWQgPC0gZGF0YS5mcmFtZShwY2FfbW9kZWxfcmVkdWNlZCR4WywgMToyXSwgY2x1c3RlciA9IGFzLmZhY3RvcihkYXRhX3JlZHVjZWQkY2x1c3RlcikpDQpnZ3Bsb3QocGNhX2RhdGFfcmVkdWNlZCwgYWVzKHggPSBQQzEsIHkgPSBQQzIsIGNvbG9yID0gY2x1c3RlcikpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgdGhlbWVfbWluaW1hbCgpICsNCiAgbGFicyh0aXRsZSA9ICJDbHVzdGVyaW5nIHZpc3VhbGl6YWRvIGVuIGVzcGFjaW8gUENBIikNCmBgYA0KDQpFbiBlc3RlIGNhc28sIHNlIHZpc3VhbGl6YW4gY2luY28gY2zDunN0ZXJlcywgbG8gcXVlIHBvZHLDrWEgaW5kaWNhciB1bmEgbWF5b3Igc2Vuc2liaWxpZGFkIGRlbCBtb2RlbG8gYWwgYWdydXBhciBsb3MgZGF0b3MgcmVkdWNpZG9zLg0KDQpMb3MgY2zDunN0ZXJlcyBlc3TDoW4gbcOhcyBjb21wYWN0b3MgZW4gZWwgY2VudHJvIGRlbCBncsOhZmljbywgY29uIG1lbm9zIHNlcGFyYWNpw7NuIGNsYXJhIGVuIGNvbXBhcmFjacOzbiBjb24gbGEgdmVyc2nDs24gY29tcGxldGEuDQoNCkV4aXN0ZW4gY2zDunN0ZXJlcyBhZGljaW9uYWxlcyAoY29tbyBlbCBjbMO6c3RlciA1LCByZXByZXNlbnRhZG8gZW4gcm9zYSksIGxvIHF1ZSBwdWVkZSBpbmRpY2FyIHVuYSBwYXJ0aWNpw7NuIG3DoXMgZm9yemFkYSBvIHNlbnNpYmlsaWRhZCBhbCBydWlkby4NCg0KSGF5IHVuIG1heW9yIHNvbGFwYW1pZW50byBlbnRyZSBsb3MgY2zDunN0ZXJlcyBlbiBjb21wYXJhY2nDs24gY29uIGVsIGdyw6FmaWNvIHNpbiByZWR1Y2Npw7NuIGRlIGRhdG9zLg0KDQpFbCByYW5nbyBkZWwgZWplIFBDMSB2YXLDrWEgZGUgLTUgYSAxNSwgbWllbnRyYXMgcXVlIFBDMiB0aWVuZSB1bmEgZGlzdHJpYnVjacOzbiBkZSAtNSBhIDE1LCBsbyBxdWUgc3VnaWVyZSB1biBjYW1iaW8gZW4gbGEgaW1wb3J0YW5jaWEgcmVsYXRpdmEgZGUgbG9zIGNvbXBvbmVudGVzIHByaW5jaXBhbGVzIGRlc3B1w6lzIGRlIGxhIHJlZHVjY2nDs24gZGUgdmFyaWFibGVzLg0KDQoNCkVuIGNvbmNsdXNpw7NuLCBzaW4gcmVkdWNjacOzbiwgbG9zIGNsw7pzdGVyZXMgc29uIG3DoXMgZGVmaW5pZG9zIHkgc2VwYXJhZG9zLCBsbyBxdWUgc3VnaWVyZSBxdWUgbGFzIHZhcmlhYmxlcyBlbGltaW5hZGFzIGNvbnRpZW5lbiBpbmZvcm1hY2nDs24gcmVsZXZhbnRlIHBhcmEgZGlzdGluZ3VpciBsb3MgZ3J1cG9zLg0KDQpDb24gcmVkdWNjacOzbiwgbG9zIGNsw7pzdGVyZXMgbXVlc3RyYW4gbWF5b3Igc29sYXBhbWllbnRvIHkgcGFyZWNlbiBtZW5vcyBkZWZpbmlkb3MsIHBvc2libGVtZW50ZSBkZWJpZG8gYSBsYSBww6lyZGlkYSBkZSBpbmZvcm1hY2nDs24gYWwgcmVkdWNpciBsYXMgdmFyaWFibGVzLg0KDQpMYSBhcGFyaWNpw7NuIGRlIHVuIHF1aW50byBjbMO6c3RlciBlbiBlbCBjb25qdW50byByZWR1Y2lkbyBpbmRpY2EgcXVlIGxhIHJlZHVjY2nDs24gcHVlZGUgaW50cm9kdWNpciBzZW5zaWJpbGlkYWQgYWwgcnVpZG8gbyByZXZlbGFyIHBhdHJvbmVzIG3DoXMgY29tcGxlam9zIChhdW5xdWUgbWVub3Mgc2lnbmlmaWNhdGl2b3MpLg0KDQo=